@@ -68,7 +68,6 @@
|
|||||||
"libphonenumber-js": "1.10.37",
|
"libphonenumber-js": "1.10.37",
|
||||||
"micro": "10.0.1",
|
"micro": "10.0.1",
|
||||||
"micro-cors": "0.1.1",
|
"micro-cors": "0.1.1",
|
||||||
"minio": "7.1.1",
|
|
||||||
"next": "13.4.3",
|
"next": "13.4.3",
|
||||||
"next-auth": "4.22.1",
|
"next-auth": "4.22.1",
|
||||||
"next-international": "0.9.5",
|
"next-international": "0.9.5",
|
||||||
@@ -106,7 +105,6 @@
|
|||||||
"@types/canvas-confetti": "1.6.0",
|
"@types/canvas-confetti": "1.6.0",
|
||||||
"@types/jsonwebtoken": "9.0.2",
|
"@types/jsonwebtoken": "9.0.2",
|
||||||
"@types/micro-cors": "0.1.3",
|
"@types/micro-cors": "0.1.3",
|
||||||
"@types/minio": "7.1.1",
|
|
||||||
"@types/node": "20.4.2",
|
"@types/node": "20.4.2",
|
||||||
"@types/nodemailer": "6.4.8",
|
"@types/nodemailer": "6.4.8",
|
||||||
"@types/nprogress": "0.2.0",
|
"@types/nprogress": "0.2.0",
|
||||||
|
|||||||
@@ -10,16 +10,17 @@ import {
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { EmojiOrImageIcon } from './EmojiOrImageIcon'
|
import { EmojiOrImageIcon } from './EmojiOrImageIcon'
|
||||||
import { ImageUploadContent } from './ImageUploadContent'
|
import { ImageUploadContent } from './ImageUploadContent'
|
||||||
|
import { FilePathUploadProps } from '@/features/upload/api/generateUploadUrl'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
uploadFilePath: string
|
uploadFileProps: FilePathUploadProps
|
||||||
icon?: string | null
|
icon?: string | null
|
||||||
onChangeIcon: (icon: string) => void
|
onChangeIcon: (icon: string) => void
|
||||||
boxSize?: string
|
boxSize?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export const EditableEmojiOrImageIcon = ({
|
export const EditableEmojiOrImageIcon = ({
|
||||||
uploadFilePath,
|
uploadFileProps,
|
||||||
icon,
|
icon,
|
||||||
onChangeIcon,
|
onChangeIcon,
|
||||||
boxSize,
|
boxSize,
|
||||||
@@ -54,7 +55,7 @@ export const EditableEmojiOrImageIcon = ({
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
<PopoverContent p="2">
|
<PopoverContent p="2">
|
||||||
<ImageUploadContent
|
<ImageUploadContent
|
||||||
filePath={uploadFilePath}
|
uploadFileProps={uploadFileProps}
|
||||||
defaultUrl={icon ?? ''}
|
defaultUrl={icon ?? ''}
|
||||||
onSubmit={onChangeIcon}
|
onSubmit={onChangeIcon}
|
||||||
excludedTabs={['giphy', 'unsplash']}
|
excludedTabs={['giphy', 'unsplash']}
|
||||||
|
|||||||
@@ -6,12 +6,12 @@ import { TextInput } from '../inputs/TextInput'
|
|||||||
import { EmojiSearchableList } from './emoji/EmojiSearchableList'
|
import { EmojiSearchableList } from './emoji/EmojiSearchableList'
|
||||||
import { UnsplashPicker } from './UnsplashPicker'
|
import { UnsplashPicker } from './UnsplashPicker'
|
||||||
import { IconPicker } from './IconPicker'
|
import { IconPicker } from './IconPicker'
|
||||||
|
import { FilePathUploadProps } from '@/features/upload/api/generateUploadUrl'
|
||||||
|
|
||||||
type Tabs = 'link' | 'upload' | 'giphy' | 'emoji' | 'unsplash' | 'icon'
|
type Tabs = 'link' | 'upload' | 'giphy' | 'emoji' | 'unsplash' | 'icon'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
filePath: string | undefined
|
uploadFileProps: FilePathUploadProps | undefined
|
||||||
includeFileName?: boolean
|
|
||||||
defaultUrl?: string
|
defaultUrl?: string
|
||||||
imageSize?: 'small' | 'regular' | 'thumb'
|
imageSize?: 'small' | 'regular' | 'thumb'
|
||||||
initialTab?: Tabs
|
initialTab?: Tabs
|
||||||
@@ -36,8 +36,7 @@ const defaultDisplayedTabs: Tabs[] = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
export const ImageUploadContent = ({
|
export const ImageUploadContent = ({
|
||||||
filePath,
|
uploadFileProps,
|
||||||
includeFileName,
|
|
||||||
defaultUrl,
|
defaultUrl,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
imageSize = 'regular',
|
imageSize = 'regular',
|
||||||
@@ -123,8 +122,7 @@ export const ImageUploadContent = ({
|
|||||||
</HStack>
|
</HStack>
|
||||||
|
|
||||||
<BodyContent
|
<BodyContent
|
||||||
filePath={filePath}
|
uploadFileProps={uploadFileProps}
|
||||||
includeFileName={includeFileName}
|
|
||||||
tab={currentTab}
|
tab={currentTab}
|
||||||
imageSize={imageSize}
|
imageSize={imageSize}
|
||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
@@ -135,15 +133,13 @@ export const ImageUploadContent = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const BodyContent = ({
|
const BodyContent = ({
|
||||||
includeFileName,
|
uploadFileProps,
|
||||||
filePath,
|
|
||||||
tab,
|
tab,
|
||||||
defaultUrl,
|
defaultUrl,
|
||||||
imageSize,
|
imageSize,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
}: {
|
}: {
|
||||||
includeFileName?: boolean
|
uploadFileProps?: FilePathUploadProps
|
||||||
filePath: string | undefined
|
|
||||||
tab: Tabs
|
tab: Tabs
|
||||||
defaultUrl?: string
|
defaultUrl?: string
|
||||||
imageSize: 'small' | 'regular' | 'thumb'
|
imageSize: 'small' | 'regular' | 'thumb'
|
||||||
@@ -151,11 +147,10 @@ const BodyContent = ({
|
|||||||
}) => {
|
}) => {
|
||||||
switch (tab) {
|
switch (tab) {
|
||||||
case 'upload': {
|
case 'upload': {
|
||||||
if (!filePath) return null
|
if (!uploadFileProps) return null
|
||||||
return (
|
return (
|
||||||
<UploadFileContent
|
<UploadFileContent
|
||||||
filePath={filePath}
|
uploadFileProps={uploadFileProps}
|
||||||
includeFileName={includeFileName}
|
|
||||||
onNewUrl={onSubmit}
|
onNewUrl={onSubmit}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
@@ -176,16 +171,14 @@ const BodyContent = ({
|
|||||||
type ContentProps = { onNewUrl: (url: string) => void }
|
type ContentProps = { onNewUrl: (url: string) => void }
|
||||||
|
|
||||||
const UploadFileContent = ({
|
const UploadFileContent = ({
|
||||||
filePath,
|
uploadFileProps,
|
||||||
includeFileName,
|
|
||||||
onNewUrl,
|
onNewUrl,
|
||||||
}: ContentProps & { filePath: string; includeFileName?: boolean }) => (
|
}: ContentProps & { uploadFileProps: FilePathUploadProps }) => (
|
||||||
<Flex justify="center" py="2">
|
<Flex justify="center" py="2">
|
||||||
<UploadButton
|
<UploadButton
|
||||||
fileType="image"
|
fileType="image"
|
||||||
filePath={filePath}
|
filePathProps={uploadFileProps}
|
||||||
onFileUploaded={onNewUrl}
|
onFileUploaded={onNewUrl}
|
||||||
includeFileName={includeFileName}
|
|
||||||
colorScheme="blue"
|
colorScheme="blue"
|
||||||
>
|
>
|
||||||
Choose an image
|
Choose an image
|
||||||
|
|||||||
@@ -1,25 +1,44 @@
|
|||||||
import { useToast } from '@/hooks/useToast'
|
import { useToast } from '@/hooks/useToast'
|
||||||
import { Button, ButtonProps, chakra } from '@chakra-ui/react'
|
import { Button, ButtonProps, chakra } from '@chakra-ui/react'
|
||||||
import { ChangeEvent, useState } from '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'
|
import { compressFile } from '@/helpers/compressFile'
|
||||||
|
|
||||||
type UploadButtonProps = {
|
type UploadButtonProps = {
|
||||||
fileType: 'image' | 'audio'
|
fileType: 'image' | 'audio'
|
||||||
filePath: string
|
filePathProps: FilePathUploadProps
|
||||||
includeFileName?: boolean
|
|
||||||
onFileUploaded: (url: string) => void
|
onFileUploaded: (url: string) => void
|
||||||
} & ButtonProps
|
} & ButtonProps
|
||||||
|
|
||||||
export const UploadButton = ({
|
export const UploadButton = ({
|
||||||
fileType,
|
fileType,
|
||||||
filePath,
|
filePathProps,
|
||||||
includeFileName,
|
|
||||||
onFileUploaded,
|
onFileUploaded,
|
||||||
...props
|
...props
|
||||||
}: UploadButtonProps) => {
|
}: UploadButtonProps) => {
|
||||||
const [isUploading, setIsUploading] = useState(false)
|
const [isUploading, setIsUploading] = useState(false)
|
||||||
const { showToast } = useToast()
|
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>) => {
|
const handleInputChange = async (e: ChangeEvent<HTMLInputElement>) => {
|
||||||
if (!e.target?.files) return
|
if (!e.target?.files) return
|
||||||
@@ -27,16 +46,11 @@ export const UploadButton = ({
|
|||||||
const file = e.target.files[0] as File | undefined
|
const file = e.target.files[0] as File | undefined
|
||||||
if (!file)
|
if (!file)
|
||||||
return showToast({ description: 'Could not read file.', status: 'error' })
|
return showToast({ description: 'Could not read file.', status: 'error' })
|
||||||
const urls = await uploadFiles({
|
setFile(await compressFile(file))
|
||||||
files: [
|
mutate({
|
||||||
{
|
filePathProps,
|
||||||
file: await compressFile(file),
|
fileType: file.type,
|
||||||
path: `public/${filePath}${includeFileName ? `/${file.name}` : ''}`,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
})
|
})
|
||||||
if (urls.length && urls[0]) onFileUploaded(urls[0] + '?v=' + Date.now())
|
|
||||||
setIsUploading(false)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -36,15 +36,20 @@ export const MyAccountForm = () => {
|
|||||||
name={user?.name ?? undefined}
|
name={user?.name ?? undefined}
|
||||||
/>
|
/>
|
||||||
<Stack>
|
<Stack>
|
||||||
|
{user?.id && (
|
||||||
<UploadButton
|
<UploadButton
|
||||||
size="sm"
|
size="sm"
|
||||||
fileType="image"
|
fileType="image"
|
||||||
filePath={`users/${user?.id}/avatar`}
|
filePathProps={{
|
||||||
|
userId: user.id,
|
||||||
|
fileName: 'avatar',
|
||||||
|
}}
|
||||||
leftIcon={<UploadIcon />}
|
leftIcon={<UploadIcon />}
|
||||||
onFileUploaded={handleFileUploaded}
|
onFileUploaded={handleFileUploaded}
|
||||||
>
|
>
|
||||||
{scopedT('changePhotoButton.label')}
|
{scopedT('changePhotoButton.label')}
|
||||||
</UploadButton>
|
</UploadButton>
|
||||||
|
)}
|
||||||
<Text color="gray.500" fontSize="sm">
|
<Text color="gray.500" fontSize="sm">
|
||||||
{scopedT('changePhotoButton.specification')}
|
{scopedT('changePhotoButton.specification')}
|
||||||
</Text>
|
</Text>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { parseDefaultGroupWithBlock } from '@typebot.io/lib/playwright/databaseH
|
|||||||
import { BubbleBlockType, defaultAudioBubbleContent } from '@typebot.io/schemas'
|
import { BubbleBlockType, defaultAudioBubbleContent } from '@typebot.io/schemas'
|
||||||
import { createId } from '@paralleldrive/cuid2'
|
import { createId } from '@paralleldrive/cuid2'
|
||||||
import { getTestAsset } from '@/test/utils/playwright'
|
import { getTestAsset } from '@/test/utils/playwright'
|
||||||
|
import { proWorkspaceId } from '@typebot.io/lib/playwright/databaseSetup'
|
||||||
|
|
||||||
const audioSampleUrl =
|
const audioSampleUrl =
|
||||||
'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3'
|
'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3'
|
||||||
@@ -30,11 +31,17 @@ test('should work as expected', async ({ page }) => {
|
|||||||
await page.setInputFiles('input[type="file"]', getTestAsset('sample.mp3'))
|
await page.setInputFiles('input[type="file"]', getTestAsset('sample.mp3'))
|
||||||
await expect(page.locator('audio')).toHaveAttribute(
|
await expect(page.locator('audio')).toHaveAttribute(
|
||||||
'src',
|
'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 page.getByRole('button', { name: 'Preview', exact: true }).click()
|
||||||
await expect(page.locator('audio')).toHaveAttribute(
|
await expect(page.locator('audio')).toHaveAttribute(
|
||||||
'src',
|
'src',
|
||||||
RegExp(`/public/typebots/${typebotId}/blocks`, 'gm')
|
RegExp(
|
||||||
|
`/public/workspaces/${proWorkspaceId}/typebots/${typebotId}/blocks`,
|
||||||
|
'gm'
|
||||||
|
)
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -5,15 +5,16 @@ import { useState } from 'react'
|
|||||||
import { UploadButton } from '@/components/ImageUploadContent/UploadButton'
|
import { UploadButton } from '@/components/ImageUploadContent/UploadButton'
|
||||||
import { SwitchWithLabel } from '@/components/inputs/SwitchWithLabel'
|
import { SwitchWithLabel } from '@/components/inputs/SwitchWithLabel'
|
||||||
import { useScopedI18n } from '@/locales'
|
import { useScopedI18n } from '@/locales'
|
||||||
|
import { FilePathUploadProps } from '@/features/upload/api/generateUploadUrl'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
fileUploadPath: string
|
uploadFileProps: FilePathUploadProps
|
||||||
content: AudioBubbleContent
|
content: AudioBubbleContent
|
||||||
onContentChange: (content: AudioBubbleContent) => void
|
onContentChange: (content: AudioBubbleContent) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AudioBubbleForm = ({
|
export const AudioBubbleForm = ({
|
||||||
fileUploadPath,
|
uploadFileProps,
|
||||||
content,
|
content,
|
||||||
onContentChange,
|
onContentChange,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
@@ -49,7 +50,7 @@ export const AudioBubbleForm = ({
|
|||||||
<Flex justify="center" py="2">
|
<Flex justify="center" py="2">
|
||||||
<UploadButton
|
<UploadButton
|
||||||
fileType="audio"
|
fileType="audio"
|
||||||
filePath={fileUploadPath}
|
filePathProps={uploadFileProps}
|
||||||
onFileUploaded={updateUrl}
|
onFileUploaded={updateUrl}
|
||||||
colorScheme="blue"
|
colorScheme="blue"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { ImageUploadContent } from '@/components/ImageUploadContent'
|
import { ImageUploadContent } from '@/components/ImageUploadContent'
|
||||||
import { TextInput } from '@/components/inputs'
|
import { TextInput } from '@/components/inputs'
|
||||||
import { SwitchWithLabel } from '@/components/inputs/SwitchWithLabel'
|
import { SwitchWithLabel } from '@/components/inputs/SwitchWithLabel'
|
||||||
|
import { FilePathUploadProps } from '@/features/upload/api/generateUploadUrl'
|
||||||
import { useScopedI18n } from '@/locales'
|
import { useScopedI18n } from '@/locales'
|
||||||
import { Stack } from '@chakra-ui/react'
|
import { Stack } from '@chakra-ui/react'
|
||||||
import { isDefined, isNotEmpty } from '@typebot.io/lib'
|
import { isDefined, isNotEmpty } from '@typebot.io/lib'
|
||||||
@@ -8,13 +9,13 @@ import { ImageBubbleBlock } from '@typebot.io/schemas'
|
|||||||
import React, { useState } from 'react'
|
import React, { useState } from 'react'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
typebotId: string
|
uploadFileProps: FilePathUploadProps
|
||||||
block: ImageBubbleBlock
|
block: ImageBubbleBlock
|
||||||
onContentChange: (content: ImageBubbleBlock['content']) => void
|
onContentChange: (content: ImageBubbleBlock['content']) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ImageBubbleSettings = ({
|
export const ImageBubbleSettings = ({
|
||||||
typebotId,
|
uploadFileProps,
|
||||||
block,
|
block,
|
||||||
onContentChange,
|
onContentChange,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
@@ -53,7 +54,7 @@ export const ImageBubbleSettings = ({
|
|||||||
return (
|
return (
|
||||||
<Stack p="2" spacing={4}>
|
<Stack p="2" spacing={4}>
|
||||||
<ImageUploadContent
|
<ImageUploadContent
|
||||||
filePath={`typebots/${typebotId}/blocks/${block.id}`}
|
uploadFileProps={uploadFileProps}
|
||||||
defaultUrl={block.content?.url}
|
defaultUrl={block.content?.url}
|
||||||
onSubmit={updateImage}
|
onSubmit={updateImage}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { parseDefaultGroupWithBlock } from '@typebot.io/lib/playwright/databaseH
|
|||||||
import { BubbleBlockType, defaultImageBubbleContent } from '@typebot.io/schemas'
|
import { BubbleBlockType, defaultImageBubbleContent } from '@typebot.io/schemas'
|
||||||
import { createId } from '@paralleldrive/cuid2'
|
import { createId } from '@paralleldrive/cuid2'
|
||||||
import { getTestAsset } from '@/test/utils/playwright'
|
import { getTestAsset } from '@/test/utils/playwright'
|
||||||
|
import { proWorkspaceId } from '@typebot.io/lib/playwright/databaseSetup'
|
||||||
|
|
||||||
const unsplashImageSrc =
|
const unsplashImageSrc =
|
||||||
'https://images.unsplash.com/photo-1504297050568-910d24c426d3?ixlib=rb-1.2.1&ixid=MnwxMjA3fDF8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1287&q=80'
|
'https://images.unsplash.com/photo-1504297050568-910d24c426d3?ixlib=rb-1.2.1&ixid=MnwxMjA3fDF8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1287&q=80'
|
||||||
@@ -29,7 +30,10 @@ test.describe.parallel('Image bubble block', () => {
|
|||||||
await page.setInputFiles('input[type="file"]', getTestAsset('avatar.jpg'))
|
await page.setInputFiles('input[type="file"]', getTestAsset('avatar.jpg'))
|
||||||
await expect(page.locator('img')).toHaveAttribute(
|
await expect(page.locator('img')).toHaveAttribute(
|
||||||
'src',
|
'src',
|
||||||
new RegExp(`/public/typebots/${typebotId}/blocks/block2`, 'gm')
|
new RegExp(
|
||||||
|
`/public/workspaces/${proWorkspaceId}/typebots/${typebotId}/blocks/block2`,
|
||||||
|
'gm'
|
||||||
|
)
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -134,6 +134,7 @@ export const PictureChoiceItemNode = ({
|
|||||||
>
|
>
|
||||||
{typebot && (
|
{typebot && (
|
||||||
<PictureChoiceItemSettings
|
<PictureChoiceItemSettings
|
||||||
|
workspaceId={typebot.workspaceId}
|
||||||
typebotId={typebot.id}
|
typebotId={typebot.id}
|
||||||
item={item}
|
item={item}
|
||||||
blockId={
|
blockId={
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import { ConditionForm } from '@/features/blocks/logic/condition/components/Cond
|
|||||||
import { Condition, LogicalOperator } from '@typebot.io/schemas'
|
import { Condition, LogicalOperator } from '@typebot.io/schemas'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
workspaceId: string
|
||||||
typebotId: string
|
typebotId: string
|
||||||
blockId: string
|
blockId: string
|
||||||
item: PictureChoiceItem
|
item: PictureChoiceItem
|
||||||
@@ -23,6 +24,7 @@ type Props = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const PictureChoiceItemSettings = ({
|
export const PictureChoiceItemSettings = ({
|
||||||
|
workspaceId,
|
||||||
typebotId,
|
typebotId,
|
||||||
blockId,
|
blockId,
|
||||||
item,
|
item,
|
||||||
@@ -69,7 +71,12 @@ export const PictureChoiceItemSettings = ({
|
|||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
<PopoverContent p="4" w="500px">
|
<PopoverContent p="4" w="500px">
|
||||||
<ImageUploadContent
|
<ImageUploadContent
|
||||||
filePath={`typebots/${typebotId}/blocks/${blockId}/items/${item.id}`}
|
uploadFileProps={{
|
||||||
|
workspaceId,
|
||||||
|
typebotId,
|
||||||
|
blockId,
|
||||||
|
itemId: item.id,
|
||||||
|
}}
|
||||||
defaultUrl={item.pictureSrc}
|
defaultUrl={item.pictureSrc}
|
||||||
onSubmit={(url) => {
|
onSubmit={(url) => {
|
||||||
updateImage(url)
|
updateImage(url)
|
||||||
|
|||||||
@@ -7,10 +7,10 @@ import { openAICredentialsSchema } from '@typebot.io/schemas/features/blocks/int
|
|||||||
import { smtpCredentialsSchema } from '@typebot.io/schemas/features/blocks/integrations/sendEmail'
|
import { smtpCredentialsSchema } from '@typebot.io/schemas/features/blocks/integrations/sendEmail'
|
||||||
import { encrypt } from '@typebot.io/lib/api/encryption'
|
import { encrypt } from '@typebot.io/lib/api/encryption'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { isWriteWorkspaceForbidden } from '@/features/workspace/helpers/isWriteWorkspaceForbidden copy'
|
|
||||||
import { whatsAppCredentialsSchema } from '@typebot.io/schemas/features/whatsapp'
|
import { whatsAppCredentialsSchema } from '@typebot.io/schemas/features/whatsapp'
|
||||||
import { Credentials } from '@typebot.io/schemas'
|
import { Credentials } from '@typebot.io/schemas'
|
||||||
import { isDefined } from '@typebot.io/lib/utils'
|
import { isDefined } from '@typebot.io/lib/utils'
|
||||||
|
import { isWriteWorkspaceForbidden } from '@/features/workspace/helpers/isWriteWorkspaceForbidden'
|
||||||
|
|
||||||
const inputShape = {
|
const inputShape = {
|
||||||
data: true,
|
data: true,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import prisma from '@/lib/prisma'
|
|||||||
import { authenticatedProcedure } from '@/helpers/server/trpc'
|
import { authenticatedProcedure } from '@/helpers/server/trpc'
|
||||||
import { TRPCError } from '@trpc/server'
|
import { TRPCError } from '@trpc/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { isWriteWorkspaceForbidden } from '@/features/workspace/helpers/isWriteWorkspaceForbidden copy'
|
import { isWriteWorkspaceForbidden } from '@/features/workspace/helpers/isWriteWorkspaceForbidden'
|
||||||
|
|
||||||
export const deleteCredentials = authenticatedProcedure
|
export const deleteCredentials = authenticatedProcedure
|
||||||
.meta({
|
.meta({
|
||||||
|
|||||||
@@ -3,9 +3,9 @@ import { authenticatedProcedure } from '@/helpers/server/trpc'
|
|||||||
import { TRPCError } from '@trpc/server'
|
import { TRPCError } from '@trpc/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { customDomainSchema } from '@typebot.io/schemas/features/customDomains'
|
import { customDomainSchema } from '@typebot.io/schemas/features/customDomains'
|
||||||
import { isWriteWorkspaceForbidden } from '@/features/workspace/helpers/isWriteWorkspaceForbidden copy'
|
|
||||||
import got, { HTTPError } from 'got'
|
import got, { HTTPError } from 'got'
|
||||||
import { env } from '@typebot.io/env'
|
import { env } from '@typebot.io/env'
|
||||||
|
import { isWriteWorkspaceForbidden } from '@/features/workspace/helpers/isWriteWorkspaceForbidden'
|
||||||
|
|
||||||
export const createCustomDomain = authenticatedProcedure
|
export const createCustomDomain = authenticatedProcedure
|
||||||
.meta({
|
.meta({
|
||||||
|
|||||||
@@ -2,9 +2,9 @@ import prisma from '@/lib/prisma'
|
|||||||
import { authenticatedProcedure } from '@/helpers/server/trpc'
|
import { authenticatedProcedure } from '@/helpers/server/trpc'
|
||||||
import { TRPCError } from '@trpc/server'
|
import { TRPCError } from '@trpc/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { isWriteWorkspaceForbidden } from '@/features/workspace/helpers/isWriteWorkspaceForbidden copy'
|
|
||||||
import got from 'got'
|
import got from 'got'
|
||||||
import { env } from '@typebot.io/env'
|
import { env } from '@typebot.io/env'
|
||||||
|
import { isWriteWorkspaceForbidden } from '@/features/workspace/helpers/isWriteWorkspaceForbidden'
|
||||||
|
|
||||||
export const deleteCustomDomain = authenticatedProcedure
|
export const deleteCustomDomain = authenticatedProcedure
|
||||||
.meta({
|
.meta({
|
||||||
|
|||||||
@@ -179,7 +179,11 @@ export const TypebotHeader = () => {
|
|||||||
<HStack spacing={1}>
|
<HStack spacing={1}>
|
||||||
{typebot && (
|
{typebot && (
|
||||||
<EditableEmojiOrImageIcon
|
<EditableEmojiOrImageIcon
|
||||||
uploadFilePath={`typebots/${typebot.id}/icon`}
|
uploadFileProps={{
|
||||||
|
workspaceId: typebot.workspaceId,
|
||||||
|
typebotId: typebot.id,
|
||||||
|
fileName: 'icon',
|
||||||
|
}}
|
||||||
icon={typebot?.icon}
|
icon={typebot?.icon}
|
||||||
onChangeIcon={handleChangeIcon}
|
onChangeIcon={handleChangeIcon}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -284,7 +284,11 @@ export const BlockNode = ({
|
|||||||
)}
|
)}
|
||||||
{typebot && isMediaBubbleBlock(block) && (
|
{typebot && isMediaBubbleBlock(block) && (
|
||||||
<MediaBubblePopoverContent
|
<MediaBubblePopoverContent
|
||||||
typebotId={typebot.id}
|
uploadFileProps={{
|
||||||
|
workspaceId: typebot.workspaceId,
|
||||||
|
typebotId: typebot.id,
|
||||||
|
blockId: block.id,
|
||||||
|
}}
|
||||||
block={block}
|
block={block}
|
||||||
onContentChange={handleContentChange}
|
onContentChange={handleContentChange}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { AudioBubbleForm } from '@/features/blocks/bubbles/audio/components/Audi
|
|||||||
import { EmbedUploadContent } from '@/features/blocks/bubbles/embed/components/EmbedUploadContent'
|
import { EmbedUploadContent } from '@/features/blocks/bubbles/embed/components/EmbedUploadContent'
|
||||||
import { ImageBubbleSettings } from '@/features/blocks/bubbles/image/components/ImageBubbleSettings'
|
import { ImageBubbleSettings } from '@/features/blocks/bubbles/image/components/ImageBubbleSettings'
|
||||||
import { VideoUploadContent } from '@/features/blocks/bubbles/video/components/VideoUploadContent'
|
import { VideoUploadContent } from '@/features/blocks/bubbles/video/components/VideoUploadContent'
|
||||||
|
import { FilePathUploadProps } from '@/features/upload/api/generateUploadUrl'
|
||||||
import {
|
import {
|
||||||
Portal,
|
Portal,
|
||||||
PopoverContent,
|
PopoverContent,
|
||||||
@@ -17,7 +18,7 @@ import {
|
|||||||
import { useRef } from 'react'
|
import { useRef } from 'react'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
typebotId: string
|
uploadFileProps: FilePathUploadProps
|
||||||
block: Exclude<BubbleBlock, TextBubbleBlock>
|
block: Exclude<BubbleBlock, TextBubbleBlock>
|
||||||
onContentChange: (content: BubbleBlockContent) => void
|
onContentChange: (content: BubbleBlockContent) => void
|
||||||
}
|
}
|
||||||
@@ -42,7 +43,7 @@ export const MediaBubblePopoverContent = (props: Props) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const MediaBubbleContent = ({
|
export const MediaBubbleContent = ({
|
||||||
typebotId,
|
uploadFileProps,
|
||||||
block,
|
block,
|
||||||
onContentChange,
|
onContentChange,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
@@ -50,7 +51,7 @@ export const MediaBubbleContent = ({
|
|||||||
case BubbleBlockType.IMAGE: {
|
case BubbleBlockType.IMAGE: {
|
||||||
return (
|
return (
|
||||||
<ImageBubbleSettings
|
<ImageBubbleSettings
|
||||||
typebotId={typebotId}
|
uploadFileProps={uploadFileProps}
|
||||||
block={block}
|
block={block}
|
||||||
onContentChange={onContentChange}
|
onContentChange={onContentChange}
|
||||||
/>
|
/>
|
||||||
@@ -76,7 +77,7 @@ export const MediaBubbleContent = ({
|
|||||||
return (
|
return (
|
||||||
<AudioBubbleForm
|
<AudioBubbleForm
|
||||||
content={block.content}
|
content={block.content}
|
||||||
fileUploadPath={`typebots/${typebotId}/blocks/${block.id}`}
|
uploadFileProps={uploadFileProps}
|
||||||
onContentChange={onContentChange}
|
onContentChange={onContentChange}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ export const ButtonThemeSettings = ({ buttonTheme, onChange }: Props) => {
|
|||||||
updateCustomIconSrc(url)
|
updateCustomIconSrc(url)
|
||||||
onClose()
|
onClose()
|
||||||
}}
|
}}
|
||||||
filePath={undefined}
|
uploadFileProps={undefined}
|
||||||
/>
|
/>
|
||||||
</PopoverContent>
|
</PopoverContent>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import { MoreInfoTooltip } from '@/components/MoreInfoTooltip'
|
|||||||
import { TextInput, Textarea } from '@/components/inputs'
|
import { TextInput, Textarea } from '@/components/inputs'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
workspaceId: string
|
||||||
typebotId: string
|
typebotId: string
|
||||||
typebotName: string
|
typebotName: string
|
||||||
metadata: Metadata
|
metadata: Metadata
|
||||||
@@ -23,6 +24,7 @@ type Props = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const MetadataForm = ({
|
export const MetadataForm = ({
|
||||||
|
workspaceId,
|
||||||
typebotId,
|
typebotId,
|
||||||
typebotName,
|
typebotName,
|
||||||
metadata,
|
metadata,
|
||||||
@@ -61,7 +63,11 @@ export const MetadataForm = ({
|
|||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
<PopoverContent p="4" w="400px">
|
<PopoverContent p="4" w="400px">
|
||||||
<ImageUploadContent
|
<ImageUploadContent
|
||||||
filePath={`typebots/${typebotId}/favIcon`}
|
uploadFileProps={{
|
||||||
|
workspaceId,
|
||||||
|
typebotId,
|
||||||
|
fileName: 'favIcon',
|
||||||
|
}}
|
||||||
defaultUrl={metadata.favIconUrl ?? ''}
|
defaultUrl={metadata.favIconUrl ?? ''}
|
||||||
onSubmit={handleFavIconSubmit}
|
onSubmit={handleFavIconSubmit}
|
||||||
excludedTabs={['giphy', 'unsplash', 'emoji']}
|
excludedTabs={['giphy', 'unsplash', 'emoji']}
|
||||||
@@ -87,7 +93,11 @@ export const MetadataForm = ({
|
|||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
<PopoverContent p="4" w="500px">
|
<PopoverContent p="4" w="500px">
|
||||||
<ImageUploadContent
|
<ImageUploadContent
|
||||||
filePath={`typebots/${typebotId}/ogImage`}
|
uploadFileProps={{
|
||||||
|
workspaceId,
|
||||||
|
typebotId,
|
||||||
|
fileName: 'ogImage',
|
||||||
|
}}
|
||||||
defaultUrl={metadata.imageUrl}
|
defaultUrl={metadata.imageUrl}
|
||||||
onSubmit={handleImageSubmit}
|
onSubmit={handleImageSubmit}
|
||||||
excludedTabs={['giphy', 'icon', 'emoji']}
|
excludedTabs={['giphy', 'icon', 'emoji']}
|
||||||
|
|||||||
@@ -94,6 +94,7 @@ export const SettingsSideMenu = () => {
|
|||||||
<AccordionPanel pb={4} px="6">
|
<AccordionPanel pb={4} px="6">
|
||||||
{typebot && (
|
{typebot && (
|
||||||
<MetadataForm
|
<MetadataForm
|
||||||
|
workspaceId={typebot.workspaceId}
|
||||||
typebotId={typebot.id}
|
typebotId={typebot.id}
|
||||||
typebotName={typebot.name}
|
typebotName={typebot.name}
|
||||||
metadata={typebot.settings.metadata}
|
metadata={typebot.settings.metadata}
|
||||||
|
|||||||
@@ -125,6 +125,7 @@ export const ThemeSideMenu = () => {
|
|||||||
<AccordionPanel pb={4}>
|
<AccordionPanel pb={4}>
|
||||||
{typebot && (
|
{typebot && (
|
||||||
<ChatThemeSettings
|
<ChatThemeSettings
|
||||||
|
workspaceId={typebot.workspaceId}
|
||||||
typebotId={typebot.id}
|
typebotId={typebot.id}
|
||||||
chatTheme={typebot.theme.chat}
|
chatTheme={typebot.theme.chat}
|
||||||
onChatThemeChange={updateChatTheme}
|
onChatThemeChange={updateChatTheme}
|
||||||
|
|||||||
@@ -17,9 +17,10 @@ import {
|
|||||||
import { ImageUploadContent } from '@/components/ImageUploadContent'
|
import { ImageUploadContent } from '@/components/ImageUploadContent'
|
||||||
import { DefaultAvatar } from '../DefaultAvatar'
|
import { DefaultAvatar } from '../DefaultAvatar'
|
||||||
import { useOutsideClick } from '@/hooks/useOutsideClick'
|
import { useOutsideClick } from '@/hooks/useOutsideClick'
|
||||||
|
import { FilePathUploadProps } from '@/features/upload/api/generateUploadUrl'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
uploadFilePath: string
|
uploadFileProps: FilePathUploadProps
|
||||||
title: string
|
title: string
|
||||||
avatarProps?: AvatarProps
|
avatarProps?: AvatarProps
|
||||||
isDefaultCheck?: boolean
|
isDefaultCheck?: boolean
|
||||||
@@ -27,7 +28,7 @@ type Props = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const AvatarForm = ({
|
export const AvatarForm = ({
|
||||||
uploadFilePath,
|
uploadFileProps,
|
||||||
title,
|
title,
|
||||||
avatarProps,
|
avatarProps,
|
||||||
isDefaultCheck = false,
|
isDefaultCheck = false,
|
||||||
@@ -90,7 +91,7 @@ export const AvatarForm = ({
|
|||||||
w="500px"
|
w="500px"
|
||||||
>
|
>
|
||||||
<ImageUploadContent
|
<ImageUploadContent
|
||||||
filePath={uploadFilePath}
|
uploadFileProps={uploadFileProps}
|
||||||
defaultUrl={avatarProps?.url}
|
defaultUrl={avatarProps?.url}
|
||||||
imageSize="thumb"
|
imageSize="thumb"
|
||||||
onSubmit={handleImageUrl}
|
onSubmit={handleImageUrl}
|
||||||
|
|||||||
@@ -19,12 +19,14 @@ import { HostBubbles } from './HostBubbles'
|
|||||||
import { InputsTheme } from './InputsTheme'
|
import { InputsTheme } from './InputsTheme'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
workspaceId: string
|
||||||
typebotId: string
|
typebotId: string
|
||||||
chatTheme: ChatTheme
|
chatTheme: ChatTheme
|
||||||
onChatThemeChange: (chatTheme: ChatTheme) => void
|
onChatThemeChange: (chatTheme: ChatTheme) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ChatThemeSettings = ({
|
export const ChatThemeSettings = ({
|
||||||
|
workspaceId,
|
||||||
typebotId,
|
typebotId,
|
||||||
chatTheme,
|
chatTheme,
|
||||||
onChatThemeChange,
|
onChatThemeChange,
|
||||||
@@ -46,14 +48,22 @@ export const ChatThemeSettings = ({
|
|||||||
return (
|
return (
|
||||||
<Stack spacing={6}>
|
<Stack spacing={6}>
|
||||||
<AvatarForm
|
<AvatarForm
|
||||||
uploadFilePath={`typebots/${typebotId}/hostAvatar`}
|
uploadFileProps={{
|
||||||
|
workspaceId,
|
||||||
|
typebotId,
|
||||||
|
fileName: 'hostAvatar',
|
||||||
|
}}
|
||||||
title="Bot avatar"
|
title="Bot avatar"
|
||||||
avatarProps={chatTheme.hostAvatar}
|
avatarProps={chatTheme.hostAvatar}
|
||||||
isDefaultCheck
|
isDefaultCheck
|
||||||
onAvatarChange={handleHostAvatarChange}
|
onAvatarChange={handleHostAvatarChange}
|
||||||
/>
|
/>
|
||||||
<AvatarForm
|
<AvatarForm
|
||||||
uploadFilePath={`typebots/${typebotId}/guestAvatar`}
|
uploadFileProps={{
|
||||||
|
workspaceId,
|
||||||
|
typebotId,
|
||||||
|
fileName: 'guestAvatar',
|
||||||
|
}}
|
||||||
title="User avatar"
|
title="User avatar"
|
||||||
avatarProps={chatTheme.guestAvatar}
|
avatarProps={chatTheme.guestAvatar}
|
||||||
onAvatarChange={handleGuestAvatarChange}
|
onAvatarChange={handleGuestAvatarChange}
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ export const BackgroundContent = ({
|
|||||||
</Flex>
|
</Flex>
|
||||||
)
|
)
|
||||||
case BackgroundType.IMAGE:
|
case BackgroundType.IMAGE:
|
||||||
|
if (!typebot) return null
|
||||||
return (
|
return (
|
||||||
<Popover isLazy placement="top">
|
<Popover isLazy placement="top">
|
||||||
<PopoverTrigger>
|
<PopoverTrigger>
|
||||||
@@ -63,7 +64,11 @@ export const BackgroundContent = ({
|
|||||||
<Portal>
|
<Portal>
|
||||||
<PopoverContent p="4" w="500px">
|
<PopoverContent p="4" w="500px">
|
||||||
<ImageUploadContent
|
<ImageUploadContent
|
||||||
filePath={`typebots/${typebot?.id}/background`}
|
uploadFileProps={{
|
||||||
|
workspaceId: typebot.workspaceId,
|
||||||
|
typebotId: typebot.id,
|
||||||
|
fileName: 'background',
|
||||||
|
}}
|
||||||
defaultUrl={background.content}
|
defaultUrl={background.content}
|
||||||
onSubmit={handleContentChange}
|
onSubmit={handleContentChange}
|
||||||
excludedTabs={['giphy', 'icon']}
|
excludedTabs={['giphy', 'icon']}
|
||||||
@@ -72,7 +77,5 @@ export const BackgroundContent = ({
|
|||||||
</Portal>
|
</Portal>
|
||||||
</Popover>
|
</Popover>
|
||||||
)
|
)
|
||||||
default:
|
|
||||||
return <></>
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export const isWriteTypebotForbidden = async (
|
|||||||
typebot: Pick<Typebot, 'workspaceId'> & {
|
typebot: Pick<Typebot, 'workspaceId'> & {
|
||||||
collaborators: Pick<CollaboratorsOnTypebots, 'userId' | 'type'>[]
|
collaborators: Pick<CollaboratorsOnTypebots, 'userId' | 'type'>[]
|
||||||
},
|
},
|
||||||
user: Pick<User, 'email' | 'id'>
|
user: Pick<User, 'id'>
|
||||||
) => {
|
) => {
|
||||||
if (
|
if (
|
||||||
typebot.collaborators.find(
|
typebot.collaborators.find(
|
||||||
|
|||||||
173
apps/builder/src/features/upload/api/generateUploadUrl.ts
Normal file
173
apps/builder/src/features/upload/api/generateUploadUrl.ts
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
import { authenticatedProcedure } from '@/helpers/server/trpc'
|
||||||
|
import { z } from 'zod'
|
||||||
|
import { env } from '@typebot.io/env'
|
||||||
|
import { TRPCError } from '@trpc/server'
|
||||||
|
import { generatePresignedUrl } from '@typebot.io/lib/s3/generatePresignedUrl'
|
||||||
|
import prisma from '@/lib/prisma'
|
||||||
|
import { isWriteWorkspaceForbidden } from '@/features/workspace/helpers/isWriteWorkspaceForbidden'
|
||||||
|
import { isWriteTypebotForbidden } from '@/features/typebot/helpers/isWriteTypebotForbidden'
|
||||||
|
|
||||||
|
const inputSchema = z.object({
|
||||||
|
filePathProps: z
|
||||||
|
.object({
|
||||||
|
workspaceId: z.string(),
|
||||||
|
typebotId: z.string(),
|
||||||
|
blockId: z.string(),
|
||||||
|
itemId: z.string().optional(),
|
||||||
|
})
|
||||||
|
.or(
|
||||||
|
z.object({
|
||||||
|
workspaceId: z.string(),
|
||||||
|
typebotId: z.string(),
|
||||||
|
fileName: z.string(),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.or(
|
||||||
|
z.object({
|
||||||
|
userId: z.string(),
|
||||||
|
fileName: z.string(),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.or(
|
||||||
|
z.object({
|
||||||
|
workspaceId: z.string(),
|
||||||
|
fileName: z.string(),
|
||||||
|
})
|
||||||
|
),
|
||||||
|
fileType: z.string().optional(),
|
||||||
|
})
|
||||||
|
|
||||||
|
export type FilePathUploadProps = z.infer<
|
||||||
|
typeof inputSchema.shape.filePathProps
|
||||||
|
>
|
||||||
|
|
||||||
|
export const generateUploadUrl = authenticatedProcedure
|
||||||
|
.meta({
|
||||||
|
openapi: {
|
||||||
|
method: 'POST',
|
||||||
|
path: '/generate-upload-url',
|
||||||
|
summary: 'Generate upload URL',
|
||||||
|
description: 'Generate the needed URL to upload a file from the client',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.input(inputSchema)
|
||||||
|
.output(
|
||||||
|
z.object({
|
||||||
|
presignedUrl: z.string(),
|
||||||
|
fileUrl: z.string(),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.mutation(async ({ input: { filePathProps, fileType }, ctx: { user } }) => {
|
||||||
|
if (!env.S3_ENDPOINT || !env.S3_ACCESS_KEY || !env.S3_SECRET_KEY)
|
||||||
|
throw new TRPCError({
|
||||||
|
code: 'INTERNAL_SERVER_ERROR',
|
||||||
|
message:
|
||||||
|
'S3 not properly configured. Missing one of those variables: S3_ENDPOINT, S3_ACCESS_KEY, S3_SECRET_KEY',
|
||||||
|
})
|
||||||
|
|
||||||
|
if ('resultId' in filePathProps && !user)
|
||||||
|
throw new TRPCError({
|
||||||
|
code: 'UNAUTHORIZED',
|
||||||
|
message: 'You must be logged in to upload a file',
|
||||||
|
})
|
||||||
|
|
||||||
|
const filePath = await parseFilePath({
|
||||||
|
authenticatedUserId: user?.id,
|
||||||
|
uploadProps: filePathProps,
|
||||||
|
})
|
||||||
|
|
||||||
|
const presignedUrl = await generatePresignedUrl({
|
||||||
|
fileType,
|
||||||
|
filePath,
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
presignedUrl,
|
||||||
|
fileUrl: env.S3_PUBLIC_CUSTOM_DOMAIN
|
||||||
|
? `${env.S3_PUBLIC_CUSTOM_DOMAIN}/${filePath}`
|
||||||
|
: presignedUrl.split('?')[0],
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
authenticatedUserId?: string
|
||||||
|
uploadProps: FilePathUploadProps
|
||||||
|
}
|
||||||
|
|
||||||
|
const parseFilePath = async ({
|
||||||
|
authenticatedUserId,
|
||||||
|
uploadProps: input,
|
||||||
|
}: Props): Promise<string> => {
|
||||||
|
if (!authenticatedUserId)
|
||||||
|
throw new TRPCError({
|
||||||
|
code: 'UNAUTHORIZED',
|
||||||
|
message: 'You must be logged in to upload this type of file',
|
||||||
|
})
|
||||||
|
if ('userId' in input) {
|
||||||
|
if (input.userId !== authenticatedUserId)
|
||||||
|
throw new TRPCError({
|
||||||
|
code: 'UNAUTHORIZED',
|
||||||
|
message: 'You are not authorized to upload a file for this user',
|
||||||
|
})
|
||||||
|
return `public/users/${input.userId}/${input.fileName}`
|
||||||
|
}
|
||||||
|
if (!('workspaceId' in input))
|
||||||
|
throw new TRPCError({
|
||||||
|
code: 'BAD_REQUEST',
|
||||||
|
message: 'workspaceId is missing',
|
||||||
|
})
|
||||||
|
if (!('typebotId' in input)) {
|
||||||
|
const workspace = await prisma.workspace.findUnique({
|
||||||
|
where: {
|
||||||
|
id: input.workspaceId,
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
members: {
|
||||||
|
select: {
|
||||||
|
userId: true,
|
||||||
|
role: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if (
|
||||||
|
!workspace ||
|
||||||
|
isWriteWorkspaceForbidden(workspace, { id: authenticatedUserId })
|
||||||
|
)
|
||||||
|
throw new TRPCError({
|
||||||
|
code: 'NOT_FOUND',
|
||||||
|
message: 'Workspace not found',
|
||||||
|
})
|
||||||
|
return `public/workspaces/${input.workspaceId}/${input.fileName}`
|
||||||
|
}
|
||||||
|
const typebot = await prisma.typebot.findUnique({
|
||||||
|
where: {
|
||||||
|
id: input.typebotId,
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
workspaceId: true,
|
||||||
|
collaborators: {
|
||||||
|
select: {
|
||||||
|
userId: true,
|
||||||
|
type: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if (
|
||||||
|
!typebot ||
|
||||||
|
(await isWriteTypebotForbidden(typebot, {
|
||||||
|
id: authenticatedUserId,
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
throw new TRPCError({
|
||||||
|
code: 'NOT_FOUND',
|
||||||
|
message: 'Typebot not found',
|
||||||
|
})
|
||||||
|
if (!('blockId' in input)) {
|
||||||
|
return `public/workspaces/${input.workspaceId}/typebots/${input.typebotId}/${input.fileName}`
|
||||||
|
}
|
||||||
|
return `public/workspaces/${input.workspaceId}/typebots/${
|
||||||
|
input.typebotId
|
||||||
|
}/blocks/${input.blockId}${input.itemId ? `/items/${input.itemId}` : ''}`
|
||||||
|
}
|
||||||
@@ -24,9 +24,7 @@ export const WorkspaceSettingsForm = ({ onClose }: { onClose: () => void }) => {
|
|||||||
updateWorkspace({ name })
|
updateWorkspace({ name })
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleChangeIcon = (icon: string) => {
|
const handleChangeIcon = (icon: string) => updateWorkspace({ icon })
|
||||||
updateWorkspace({ icon })
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleDeleteClick = async () => {
|
const handleDeleteClick = async () => {
|
||||||
await deleteCurrentWorkspace()
|
await deleteCurrentWorkspace()
|
||||||
@@ -40,7 +38,10 @@ export const WorkspaceSettingsForm = ({ onClose }: { onClose: () => void }) => {
|
|||||||
<Flex>
|
<Flex>
|
||||||
{workspace && (
|
{workspace && (
|
||||||
<EditableEmojiOrImageIcon
|
<EditableEmojiOrImageIcon
|
||||||
uploadFilePath={`workspaces/${workspace.id}/icon`}
|
uploadFileProps={{
|
||||||
|
workspaceId: workspace.id,
|
||||||
|
fileName: 'icon',
|
||||||
|
}}
|
||||||
icon={workspace.icon}
|
icon={workspace.icon}
|
||||||
onChangeIcon={handleChangeIcon}
|
onChangeIcon={handleChangeIcon}
|
||||||
boxSize="40px"
|
boxSize="40px"
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ export const isWriteWorkspaceForbidden = (
|
|||||||
workspace: {
|
workspace: {
|
||||||
members: Pick<MemberInWorkspace, 'userId' | 'role'>[]
|
members: Pick<MemberInWorkspace, 'userId' | 'role'>[]
|
||||||
},
|
},
|
||||||
user: Pick<User, 'email' | 'id'>
|
user: Pick<User, 'id'>
|
||||||
) => {
|
) => {
|
||||||
const userRole = workspace.members.find(
|
const userRole = workspace.members.find(
|
||||||
(member) => member.userId === user.id
|
(member) => member.userId === user.id
|
||||||
@@ -15,6 +15,7 @@ import { collaboratorsRouter } from '@/features/collaboration/api/router'
|
|||||||
import { customDomainsRouter } from '@/features/customDomains/api/router'
|
import { customDomainsRouter } from '@/features/customDomains/api/router'
|
||||||
import { whatsAppRouter } from '@/features/whatsapp/router'
|
import { whatsAppRouter } from '@/features/whatsapp/router'
|
||||||
import { openAIRouter } from '@/features/blocks/integrations/openai/api/router'
|
import { openAIRouter } from '@/features/blocks/integrations/openai/api/router'
|
||||||
|
import { generateUploadUrl } from '@/features/upload/api/generateUploadUrl'
|
||||||
|
|
||||||
export const trpcRouter = router({
|
export const trpcRouter = router({
|
||||||
getAppVersionProcedure,
|
getAppVersionProcedure,
|
||||||
@@ -33,6 +34,7 @@ export const trpcRouter = router({
|
|||||||
customDomains: customDomainsRouter,
|
customDomains: customDomainsRouter,
|
||||||
whatsApp: whatsAppRouter,
|
whatsApp: whatsAppRouter,
|
||||||
openAI: openAIRouter,
|
openAI: openAIRouter,
|
||||||
|
generateUploadUrl,
|
||||||
})
|
})
|
||||||
|
|
||||||
export type AppRouter = typeof trpcRouter
|
export type AppRouter = typeof trpcRouter
|
||||||
|
|||||||
@@ -134,7 +134,7 @@ For `*_PATH` parameters, you can use dot notation to access nested properties (i
|
|||||||
Used for uploading images, videos, etc... It can be any S3 compatible object storage service (Minio, Digital Oceans Space, AWS S3...)
|
Used for uploading images, videos, etc... It can be any S3 compatible object storage service (Minio, Digital Oceans Space, AWS S3...)
|
||||||
|
|
||||||
| Parameter | Default | Description |
|
| Parameter | Default | Description |
|
||||||
| ------------- | ------- | -------------------------------------------------------------- |
|
| ----------------------- | ------- | ---------------------------------------------------------------------------------- |
|
||||||
| S3_ACCESS_KEY | | S3 access key. Also used to check if upload feature is enabled |
|
| S3_ACCESS_KEY | | S3 access key. Also used to check if upload feature is enabled |
|
||||||
| S3_SECRET_KEY | | S3 secret key. |
|
| S3_SECRET_KEY | | S3 secret key. |
|
||||||
| S3_BUCKET | typebot | Name of the bucket where assets will be uploaded in. |
|
| S3_BUCKET | typebot | Name of the bucket where assets will be uploaded in. |
|
||||||
@@ -142,6 +142,7 @@ Used for uploading images, videos, etc... It can be any S3 compatible object sto
|
|||||||
| S3_ENDPOINT | | S3 endpoint (i.e. `s3.domain.com`). |
|
| S3_ENDPOINT | | S3 endpoint (i.e. `s3.domain.com`). |
|
||||||
| S3_SSL | true | Use SSL when establishing the connection. |
|
| S3_SSL | true | Use SSL when establishing the connection. |
|
||||||
| S3_REGION | | S3 region. |
|
| S3_REGION | | S3 region. |
|
||||||
|
| S3_PUBLIC_CUSTOM_DOMAIN | | If the final URL that is used to read public files is different from `S3_ENDPOINT` |
|
||||||
|
|
||||||
Note that for AWS S3, your endpoint is usually: `s3.<S3_REGION>.amazonaws.com`
|
Note that for AWS S3, your endpoint is usually: `s3.<S3_REGION>.amazonaws.com`
|
||||||
|
|
||||||
|
|||||||
@@ -32214,6 +32214,148 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"/generate-upload-url": {
|
||||||
|
"post": {
|
||||||
|
"operationId": "generateUploadUrl",
|
||||||
|
"summary": "Generate upload URL",
|
||||||
|
"description": "Generate the needed URL to upload a file from the client",
|
||||||
|
"requestBody": {
|
||||||
|
"required": true,
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"filePathProps": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"workspaceId": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"typebotId": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"blockId": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"itemId": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"workspaceId",
|
||||||
|
"typebotId",
|
||||||
|
"blockId"
|
||||||
|
],
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"workspaceId": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"typebotId": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"fileName": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"workspaceId",
|
||||||
|
"typebotId",
|
||||||
|
"fileName"
|
||||||
|
],
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"userId": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"fileName": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"userId",
|
||||||
|
"fileName"
|
||||||
|
],
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"workspaceId": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"fileName": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"workspaceId",
|
||||||
|
"fileName"
|
||||||
|
],
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"fileType": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"filePathProps"
|
||||||
|
],
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"parameters": [],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Successful response",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"presignedUrl": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"fileUrl": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"presignedUrl",
|
||||||
|
"fileUrl"
|
||||||
|
],
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"default": {
|
||||||
|
"$ref": "#/components/responses/error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"components": {
|
"components": {
|
||||||
|
|||||||
@@ -6282,6 +6282,86 @@
|
|||||||
"default": {
|
"default": {
|
||||||
"$ref": "#/components/responses/error"
|
"$ref": "#/components/responses/error"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"deprecated": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/generate-upload-url": {
|
||||||
|
"post": {
|
||||||
|
"operationId": "generateUploadUrl",
|
||||||
|
"summary": "Generate upload URL",
|
||||||
|
"description": "Used to upload anything from the client to S3 bucket",
|
||||||
|
"requestBody": {
|
||||||
|
"required": true,
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"filePathProps": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"typebotId": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"blockId": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"resultId": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"fileName": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"typebotId",
|
||||||
|
"blockId",
|
||||||
|
"resultId",
|
||||||
|
"fileName"
|
||||||
|
],
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"fileType": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"filePathProps"
|
||||||
|
],
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"parameters": [],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Successful response",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"presignedUrl": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"fileUrl": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"presignedUrl",
|
||||||
|
"fileUrl"
|
||||||
|
],
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"default": {
|
||||||
|
"$ref": "#/components/responses/error"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ export const getUploadUrl = publicProcedure
|
|||||||
path: '/typebots/{typebotId}/blocks/{blockId}/storage/upload-url',
|
path: '/typebots/{typebotId}/blocks/{blockId}/storage/upload-url',
|
||||||
summary: 'Get upload URL for a file',
|
summary: 'Get upload URL for a file',
|
||||||
description: 'Used for the web client to get the bucket upload file.',
|
description: 'Used for the web client to get the bucket upload file.',
|
||||||
|
deprecated: true,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.input(
|
.input(
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
import { publicProcedure } from '@/helpers/server/trpc'
|
||||||
|
import prisma from '@/lib/prisma'
|
||||||
|
import { TRPCError } from '@trpc/server'
|
||||||
|
import { z } from 'zod'
|
||||||
|
import { generatePresignedUrl } from '@typebot.io/lib/s3/generatePresignedUrl'
|
||||||
|
import { env } from '@typebot.io/env'
|
||||||
|
|
||||||
|
export const generateUploadUrl = publicProcedure
|
||||||
|
.meta({
|
||||||
|
openapi: {
|
||||||
|
method: 'POST',
|
||||||
|
path: '/generate-upload-url',
|
||||||
|
summary: 'Generate upload URL',
|
||||||
|
description: 'Used to upload anything from the client to S3 bucket',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.input(
|
||||||
|
z.object({
|
||||||
|
filePathProps: z.object({
|
||||||
|
typebotId: z.string(),
|
||||||
|
blockId: z.string(),
|
||||||
|
resultId: z.string(),
|
||||||
|
fileName: z.string(),
|
||||||
|
}),
|
||||||
|
fileType: z.string().optional(),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.output(
|
||||||
|
z.object({
|
||||||
|
presignedUrl: z.string(),
|
||||||
|
fileUrl: z.string(),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.mutation(async ({ input: { filePathProps, fileType } }) => {
|
||||||
|
if (!env.S3_ENDPOINT || !env.S3_ACCESS_KEY || !env.S3_SECRET_KEY)
|
||||||
|
throw new TRPCError({
|
||||||
|
code: 'INTERNAL_SERVER_ERROR',
|
||||||
|
message:
|
||||||
|
'S3 not properly configured. Missing one of those variables: S3_ENDPOINT, S3_ACCESS_KEY, S3_SECRET_KEY',
|
||||||
|
})
|
||||||
|
|
||||||
|
const publicTypebot = await prisma.publicTypebot.findFirst({
|
||||||
|
where: {
|
||||||
|
typebotId: filePathProps.typebotId,
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
typebot: {
|
||||||
|
select: {
|
||||||
|
workspaceId: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const workspaceId = publicTypebot?.typebot.workspaceId
|
||||||
|
|
||||||
|
if (!workspaceId)
|
||||||
|
throw new TRPCError({
|
||||||
|
code: 'BAD_REQUEST',
|
||||||
|
message: "Can't find workspaceId",
|
||||||
|
})
|
||||||
|
|
||||||
|
const filePath = `public/workspaces/${workspaceId}/typebots/${filePathProps.typebotId}/results/${filePathProps.resultId}/${filePathProps.fileName}`
|
||||||
|
|
||||||
|
const presignedUrl = await generatePresignedUrl({
|
||||||
|
fileType,
|
||||||
|
filePath,
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
presignedUrl,
|
||||||
|
fileUrl: env.S3_PUBLIC_CUSTOM_DOMAIN
|
||||||
|
? `${env.S3_PUBLIC_CUSTOM_DOMAIN}/${filePath}`
|
||||||
|
: presignedUrl.split('?')[0],
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { getUploadUrl } from '@/features/blocks/inputs/fileUpload/api/getUploadUrl'
|
import { getUploadUrl } from '@/features/blocks/inputs/fileUpload/api/deprecated/getUploadUrl'
|
||||||
|
import { generateUploadUrl } from '@/features/blocks/inputs/fileUpload/api/generateUploadUrl'
|
||||||
import { sendMessage } from '@/features/chat/api/sendMessage'
|
import { sendMessage } from '@/features/chat/api/sendMessage'
|
||||||
import { whatsAppRouter } from '@/features/whatsApp/api/router'
|
import { whatsAppRouter } from '@/features/whatsApp/api/router'
|
||||||
import { router } from '../../trpc'
|
import { router } from '../../trpc'
|
||||||
@@ -7,6 +8,7 @@ import { updateTypebotInSession } from '@/features/chat/api/updateTypebotInSessi
|
|||||||
export const appRouter = router({
|
export const appRouter = router({
|
||||||
sendMessage,
|
sendMessage,
|
||||||
getUploadUrl,
|
getUploadUrl,
|
||||||
|
generateUploadUrl,
|
||||||
updateTypebotInSession,
|
updateTypebotInSession,
|
||||||
whatsAppRouter,
|
whatsAppRouter,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { useTypebot } from '@/providers/TypebotProvider'
|
|||||||
import { InputSubmitContent } from '@/types'
|
import { InputSubmitContent } from '@/types'
|
||||||
import { defaultFileInputOptions, FileInputBlock } from '@typebot.io/schemas'
|
import { defaultFileInputOptions, FileInputBlock } from '@typebot.io/schemas'
|
||||||
import React, { ChangeEvent, FormEvent, useState, DragEvent } from 'react'
|
import React, { ChangeEvent, FormEvent, useState, DragEvent } from 'react'
|
||||||
import { uploadFiles } from '@typebot.io/lib/s3/uploadFiles'
|
import { uploadFiles } from '../helpers/uploadFiles'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
block: FileInputBlock
|
block: FileInputBlock
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { sendRequest } from '../utils'
|
import { sendRequest } from '@typebot.io/lib/utils'
|
||||||
|
|
||||||
type UploadFileProps = {
|
type UploadFileProps = {
|
||||||
basePath?: string
|
basePath?: string
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@typebot.io/js",
|
"name": "@typebot.io/js",
|
||||||
"version": "0.1.25",
|
"version": "0.1.26",
|
||||||
"description": "Javascript library to display typebots on your website",
|
"description": "Javascript library to display typebots on your website",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import { SendButton } from '@/components/SendButton'
|
import { SendButton } from '@/components/SendButton'
|
||||||
import { BotContext, InputSubmitContent } from '@/types'
|
import { BotContext, InputSubmitContent } from '@/types'
|
||||||
import { guessApiHost } from '@/utils/guessApiHost'
|
|
||||||
import { FileInputBlock } from '@typebot.io/schemas'
|
import { FileInputBlock } from '@typebot.io/schemas'
|
||||||
import { defaultFileInputOptions } from '@typebot.io/schemas/features/blocks/inputs/file'
|
import { defaultFileInputOptions } from '@typebot.io/schemas/features/blocks/inputs/file'
|
||||||
import { createSignal, Match, Show, Switch } from 'solid-js'
|
import { createSignal, Match, Show, Switch } from 'solid-js'
|
||||||
import { uploadFiles } from '@typebot.io/lib/s3/uploadFiles'
|
|
||||||
import { Button } from '@/components/Button'
|
import { Button } from '@/components/Button'
|
||||||
import { Spinner } from '@/components/Spinner'
|
import { Spinner } from '@/components/Spinner'
|
||||||
|
import { uploadFiles } from '../helpers/uploadFiles'
|
||||||
|
import { guessApiHost } from '@/utils/guessApiHost'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
context: BotContext
|
context: BotContext
|
||||||
@@ -46,20 +46,23 @@ export const FileUploadForm = (props: Props) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const startSingleFileUpload = async (file: File) => {
|
const startSingleFileUpload = async (file: File) => {
|
||||||
if (props.context.isPreview)
|
if (props.context.isPreview || !props.context.resultId)
|
||||||
return props.onSubmit({
|
return props.onSubmit({
|
||||||
label: `File uploaded`,
|
label: `File uploaded`,
|
||||||
value: 'http://fake-upload-url.com',
|
value: 'http://fake-upload-url.com',
|
||||||
})
|
})
|
||||||
setIsUploading(true)
|
setIsUploading(true)
|
||||||
const urls = await uploadFiles({
|
const urls = await uploadFiles({
|
||||||
basePath: `${props.context.apiHost ?? guessApiHost()}/api/typebots/${
|
apiHost: props.context.apiHost ?? guessApiHost(),
|
||||||
props.context.typebot.id
|
|
||||||
}/blocks/${props.block.id}`,
|
|
||||||
files: [
|
files: [
|
||||||
{
|
{
|
||||||
file,
|
file,
|
||||||
path: `public/results/${props.context.resultId}/${props.block.id}/${file.name}`,
|
input: {
|
||||||
|
resultId: props.context.resultId,
|
||||||
|
typebotId: props.context.typebot.id,
|
||||||
|
blockId: props.block.id,
|
||||||
|
fileName: file.name,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
@@ -69,7 +72,8 @@ export const FileUploadForm = (props: Props) => {
|
|||||||
setErrorMessage('An error occured while uploading the file')
|
setErrorMessage('An error occured while uploading the file')
|
||||||
}
|
}
|
||||||
const startFilesUpload = async (files: File[]) => {
|
const startFilesUpload = async (files: File[]) => {
|
||||||
if (props.context.isPreview)
|
const resultId = props.context.resultId
|
||||||
|
if (props.context.isPreview || !resultId)
|
||||||
return props.onSubmit({
|
return props.onSubmit({
|
||||||
label: `${files.length} file${files.length > 1 ? 's' : ''} uploaded`,
|
label: `${files.length} file${files.length > 1 ? 's' : ''} uploaded`,
|
||||||
value: files
|
value: files
|
||||||
@@ -78,12 +82,15 @@ export const FileUploadForm = (props: Props) => {
|
|||||||
})
|
})
|
||||||
setIsUploading(true)
|
setIsUploading(true)
|
||||||
const urls = await uploadFiles({
|
const urls = await uploadFiles({
|
||||||
basePath: `${props.context.apiHost ?? guessApiHost()}/api/typebots/${
|
apiHost: props.context.apiHost ?? guessApiHost(),
|
||||||
props.context.typebot.id
|
|
||||||
}/blocks/${props.block.id}`,
|
|
||||||
files: files.map((file) => ({
|
files: files.map((file) => ({
|
||||||
file: file,
|
file: file,
|
||||||
path: `public/results/${props.context.resultId}/${props.block.id}/${file.name}`,
|
input: {
|
||||||
|
resultId,
|
||||||
|
typebotId: props.context.typebot.id,
|
||||||
|
blockId: props.block.id,
|
||||||
|
fileName: file.name,
|
||||||
|
},
|
||||||
})),
|
})),
|
||||||
onUploadProgress: setUploadProgressPercent,
|
onUploadProgress: setUploadProgressPercent,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import { sendRequest } from '@typebot.io/lib/utils'
|
||||||
|
|
||||||
|
type UploadFileProps = {
|
||||||
|
apiHost: string
|
||||||
|
files: {
|
||||||
|
file: File
|
||||||
|
input: {
|
||||||
|
typebotId: string
|
||||||
|
blockId: string
|
||||||
|
resultId: string
|
||||||
|
fileName: string
|
||||||
|
}
|
||||||
|
}[]
|
||||||
|
onUploadProgress?: (percent: number) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
type UrlList = (string | null)[]
|
||||||
|
|
||||||
|
export const uploadFiles = async ({
|
||||||
|
apiHost,
|
||||||
|
files,
|
||||||
|
onUploadProgress,
|
||||||
|
}: UploadFileProps): Promise<UrlList> => {
|
||||||
|
const urls = []
|
||||||
|
let i = 0
|
||||||
|
for (const { input, file } of files) {
|
||||||
|
onUploadProgress && onUploadProgress((i / files.length) * 100)
|
||||||
|
i += 1
|
||||||
|
const { data } = await sendRequest<{
|
||||||
|
presignedUrl: string
|
||||||
|
fileUrl: string
|
||||||
|
}>({
|
||||||
|
method: 'POST',
|
||||||
|
url: `${apiHost}/api/v1/generate-upload-url`,
|
||||||
|
body: {
|
||||||
|
filePathProps: input,
|
||||||
|
fileType: file.type,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!data?.presignedUrl) continue
|
||||||
|
else {
|
||||||
|
const upload = await fetch(data.presignedUrl, {
|
||||||
|
method: 'PUT',
|
||||||
|
body: file,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!upload.ok) continue
|
||||||
|
|
||||||
|
urls.push(data.fileUrl)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return urls
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@typebot.io/nextjs",
|
"name": "@typebot.io/nextjs",
|
||||||
"version": "0.1.25",
|
"version": "0.1.26",
|
||||||
"description": "Convenient library to display typebots on your Next.js website",
|
"description": "Convenient library to display typebots on your Next.js website",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"types": "dist/index.d.ts",
|
"types": "dist/index.d.ts",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@typebot.io/react",
|
"name": "@typebot.io/react",
|
||||||
"version": "0.1.25",
|
"version": "0.1.26",
|
||||||
"description": "Convenient library to display typebots on your React app",
|
"description": "Convenient library to display typebots on your React app",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"types": "dist/index.d.ts",
|
"types": "dist/index.d.ts",
|
||||||
|
|||||||
1
packages/env/env.ts
vendored
1
packages/env/env.ts
vendored
@@ -170,6 +170,7 @@ const s3Env = {
|
|||||||
S3_ENDPOINT: z.string().min(1).optional(),
|
S3_ENDPOINT: z.string().min(1).optional(),
|
||||||
S3_SSL: boolean.optional().default('true'),
|
S3_SSL: boolean.optional().default('true'),
|
||||||
S3_REGION: z.string().min(1).optional(),
|
S3_REGION: z.string().min(1).optional(),
|
||||||
|
S3_PUBLIC_CUSTOM_DOMAIN: z.string().url().optional(),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,6 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"got": "12.6.0",
|
"got": "12.6.0",
|
||||||
"minio": "7.1.1"
|
"minio": "7.1.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
42
packages/lib/s3/getFolderSize.ts
Normal file
42
packages/lib/s3/getFolderSize.ts
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import { env } from '@typebot.io/env'
|
||||||
|
import { Client } from 'minio'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
folderPath: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getFolderSize = async ({ folderPath }: Props) => {
|
||||||
|
if (!env.S3_ENDPOINT || !env.S3_ACCESS_KEY || !env.S3_SECRET_KEY)
|
||||||
|
throw new Error(
|
||||||
|
'S3 not properly configured. Missing one of those variables: S3_ENDPOINT, S3_ACCESS_KEY, S3_SECRET_KEY'
|
||||||
|
)
|
||||||
|
|
||||||
|
const minioClient = new Client({
|
||||||
|
endPoint: env.S3_ENDPOINT,
|
||||||
|
port: env.S3_PORT,
|
||||||
|
useSSL: env.S3_SSL,
|
||||||
|
accessKey: env.S3_ACCESS_KEY,
|
||||||
|
secretKey: env.S3_SECRET_KEY,
|
||||||
|
region: env.S3_REGION,
|
||||||
|
})
|
||||||
|
|
||||||
|
return new Promise<number>((resolve, reject) => {
|
||||||
|
let totalSize = 0
|
||||||
|
|
||||||
|
const stream = minioClient.listObjectsV2(
|
||||||
|
env.S3_BUCKET,
|
||||||
|
'public/' + folderPath,
|
||||||
|
true
|
||||||
|
)
|
||||||
|
|
||||||
|
stream.on('data', function (obj) {
|
||||||
|
totalSize += obj.size
|
||||||
|
})
|
||||||
|
stream.on('error', function (err) {
|
||||||
|
reject(err)
|
||||||
|
})
|
||||||
|
stream.on('end', function () {
|
||||||
|
resolve(totalSize)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
47
pnpm-lock.yaml
generated
47
pnpm-lock.yaml
generated
@@ -197,9 +197,6 @@ importers:
|
|||||||
micro-cors:
|
micro-cors:
|
||||||
specifier: 0.1.1
|
specifier: 0.1.1
|
||||||
version: 0.1.1
|
version: 0.1.1
|
||||||
minio:
|
|
||||||
specifier: 7.1.1
|
|
||||||
version: 7.1.1
|
|
||||||
next:
|
next:
|
||||||
specifier: 13.4.3
|
specifier: 13.4.3
|
||||||
version: 13.4.3(@babel/core@7.22.9)(react-dom@18.2.0)(react@18.2.0)
|
version: 13.4.3(@babel/core@7.22.9)(react-dom@18.2.0)(react@18.2.0)
|
||||||
@@ -306,9 +303,6 @@ importers:
|
|||||||
'@types/micro-cors':
|
'@types/micro-cors':
|
||||||
specifier: 0.1.3
|
specifier: 0.1.3
|
||||||
version: 0.1.3
|
version: 0.1.3
|
||||||
'@types/minio':
|
|
||||||
specifier: 7.1.1
|
|
||||||
version: 7.1.1
|
|
||||||
'@types/node':
|
'@types/node':
|
||||||
specifier: 20.4.2
|
specifier: 20.4.2
|
||||||
version: 20.4.2
|
version: 20.4.2
|
||||||
@@ -1128,8 +1122,8 @@ importers:
|
|||||||
specifier: 12.6.0
|
specifier: 12.6.0
|
||||||
version: 12.6.0
|
version: 12.6.0
|
||||||
minio:
|
minio:
|
||||||
specifier: 7.1.1
|
specifier: 7.1.3
|
||||||
version: 7.1.1
|
version: 7.1.3
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@paralleldrive/cuid2':
|
'@paralleldrive/cuid2':
|
||||||
specifier: 2.2.1
|
specifier: 2.2.1
|
||||||
@@ -8686,13 +8680,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA==}
|
resolution: {integrity: sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/@types/minio@7.1.1:
|
|
||||||
resolution: {integrity: sha512-B7OWB7JwIxVBxypiS3gA96gaK4yo2UknGdqmuQsTccZZ/ABiQ2F3fTe9lZIXL6ZuN23l+mWIC3J4CefKNyWjxA==}
|
|
||||||
deprecated: This is a stub types definition. minio provides its own type definitions, so you do not need this installed.
|
|
||||||
dependencies:
|
|
||||||
minio: 7.1.1
|
|
||||||
dev: true
|
|
||||||
|
|
||||||
/@types/node@17.0.45:
|
/@types/node@17.0.45:
|
||||||
resolution: {integrity: sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw==}
|
resolution: {integrity: sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw==}
|
||||||
dev: false
|
dev: false
|
||||||
@@ -10035,6 +10022,7 @@ packages:
|
|||||||
/@zxing/text-encoding@0.9.0:
|
/@zxing/text-encoding@0.9.0:
|
||||||
resolution: {integrity: sha512-U/4aVJ2mxI0aDNI8Uq0wEhMgY+u4CNtEb0om3+y3+niDAsoTCOB33UF0sxpzqzdqXLqmvc+vZyAt4O8pPdfkwA==}
|
resolution: {integrity: sha512-U/4aVJ2mxI0aDNI8Uq0wEhMgY+u4CNtEb0om3+y3+niDAsoTCOB33UF0sxpzqzdqXLqmvc+vZyAt4O8pPdfkwA==}
|
||||||
requiresBuild: true
|
requiresBuild: true
|
||||||
|
dev: false
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
/abab@2.0.6:
|
/abab@2.0.6:
|
||||||
@@ -10467,6 +10455,7 @@ packages:
|
|||||||
|
|
||||||
/async@3.2.4:
|
/async@3.2.4:
|
||||||
resolution: {integrity: sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==}
|
resolution: {integrity: sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/asynckit@0.4.0:
|
/asynckit@0.4.0:
|
||||||
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
|
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
|
||||||
@@ -10817,6 +10806,7 @@ packages:
|
|||||||
resolution: {integrity: sha512-suhjmLI57Ewpmq00qaygS8UgEq2ly2PCItenIyhMqVjo4t4pGzqMvfgJuX8iWTeSDdfSSqS6j38fL4ToNL7Pfg==}
|
resolution: {integrity: sha512-suhjmLI57Ewpmq00qaygS8UgEq2ly2PCItenIyhMqVjo4t4pGzqMvfgJuX8iWTeSDdfSSqS6j38fL4ToNL7Pfg==}
|
||||||
dependencies:
|
dependencies:
|
||||||
readable-stream: 3.6.2
|
readable-stream: 3.6.2
|
||||||
|
dev: false
|
||||||
|
|
||||||
/body-parser@1.20.1:
|
/body-parser@1.20.1:
|
||||||
resolution: {integrity: sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==}
|
resolution: {integrity: sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==}
|
||||||
@@ -10924,6 +10914,7 @@ packages:
|
|||||||
|
|
||||||
/browser-or-node@2.1.1:
|
/browser-or-node@2.1.1:
|
||||||
resolution: {integrity: sha512-8CVjaLJGuSKMVTxJ2DpBl5XnlNDiT4cQFeuCJJrvJmts9YrTZDizTX7PjC2s6W4x+MBGZeEY6dGMrF04/6Hgqg==}
|
resolution: {integrity: sha512-8CVjaLJGuSKMVTxJ2DpBl5XnlNDiT4cQFeuCJJrvJmts9YrTZDizTX7PjC2s6W4x+MBGZeEY6dGMrF04/6Hgqg==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/browserslist@4.21.10:
|
/browserslist@4.21.10:
|
||||||
resolution: {integrity: sha512-bipEBdZfVH5/pwrvqc+Ub0kUPVfGUhlKxbvfD+z1BDnPEO/X98ruXGA1WP5ASpAFKan7Qr6j736IacbZQuAlKQ==}
|
resolution: {integrity: sha512-bipEBdZfVH5/pwrvqc+Ub0kUPVfGUhlKxbvfD+z1BDnPEO/X98ruXGA1WP5ASpAFKan7Qr6j736IacbZQuAlKQ==}
|
||||||
@@ -10950,6 +10941,7 @@ packages:
|
|||||||
|
|
||||||
/buffer-crc32@0.2.13:
|
/buffer-crc32@0.2.13:
|
||||||
resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==}
|
resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/buffer-equal-constant-time@1.0.1:
|
/buffer-equal-constant-time@1.0.1:
|
||||||
resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==}
|
resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==}
|
||||||
@@ -12115,6 +12107,7 @@ packages:
|
|||||||
/decode-uri-component@0.2.2:
|
/decode-uri-component@0.2.2:
|
||||||
resolution: {integrity: sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==}
|
resolution: {integrity: sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==}
|
||||||
engines: {node: '>=0.10'}
|
engines: {node: '>=0.10'}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/decode-uri-component@0.4.1:
|
/decode-uri-component@0.4.1:
|
||||||
resolution: {integrity: sha512-+8VxcR21HhTy8nOt6jf20w0c9CADrw1O8d+VZ/YzzCt4bJ3uBjw+D1q2osAB8RnpwwaeYBxy0HyKQxD5JBMuuQ==}
|
resolution: {integrity: sha512-+8VxcR21HhTy8nOt6jf20w0c9CADrw1O8d+VZ/YzzCt4bJ3uBjw+D1q2osAB8RnpwwaeYBxy0HyKQxD5JBMuuQ==}
|
||||||
@@ -13628,6 +13621,7 @@ packages:
|
|||||||
hasBin: true
|
hasBin: true
|
||||||
dependencies:
|
dependencies:
|
||||||
strnum: 1.0.5
|
strnum: 1.0.5
|
||||||
|
dev: false
|
||||||
|
|
||||||
/fastest-stable-stringify@2.0.2:
|
/fastest-stable-stringify@2.0.2:
|
||||||
resolution: {integrity: sha512-bijHueCGd0LqqNK9b5oCMHc0MluJAx0cwqASgbWMvkO01lCYgIhacVRLcaDz3QnyYIRNJRDwMb41VuT6pHJ91Q==}
|
resolution: {integrity: sha512-bijHueCGd0LqqNK9b5oCMHc0MluJAx0cwqASgbWMvkO01lCYgIhacVRLcaDz3QnyYIRNJRDwMb41VuT6pHJ91Q==}
|
||||||
@@ -13739,6 +13733,7 @@ packages:
|
|||||||
/filter-obj@1.1.0:
|
/filter-obj@1.1.0:
|
||||||
resolution: {integrity: sha512-8rXg1ZnX7xzy2NGDVkBVaAy+lSlPNwad13BtgSlLuxfIslyt5Vg64U7tFcCt4WS1R0hvtnQybT/IyCkGZ3DpXQ==}
|
resolution: {integrity: sha512-8rXg1ZnX7xzy2NGDVkBVaAy+lSlPNwad13BtgSlLuxfIslyt5Vg64U7tFcCt4WS1R0hvtnQybT/IyCkGZ3DpXQ==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/filter-obj@5.1.0:
|
/filter-obj@5.1.0:
|
||||||
resolution: {integrity: sha512-qWeTREPoT7I0bifpPUXtxkZJ1XJzxWtfoWWkdVGqa+eCr3SHW/Ocp89o8vLvbUuQnadybJpjOKu4V+RwO6sGng==}
|
resolution: {integrity: sha512-qWeTREPoT7I0bifpPUXtxkZJ1XJzxWtfoWWkdVGqa+eCr3SHW/Ocp89o8vLvbUuQnadybJpjOKu4V+RwO6sGng==}
|
||||||
@@ -15070,6 +15065,7 @@ packages:
|
|||||||
/ipaddr.js@2.1.0:
|
/ipaddr.js@2.1.0:
|
||||||
resolution: {integrity: sha512-LlbxQ7xKzfBusov6UMi4MFpEg0m+mAm9xyNGEduwXMEDuf4WfzB/RZwMVYEd7IKGvh4IUkEXYxtAVu9T3OelJQ==}
|
resolution: {integrity: sha512-LlbxQ7xKzfBusov6UMi4MFpEg0m+mAm9xyNGEduwXMEDuf4WfzB/RZwMVYEd7IKGvh4IUkEXYxtAVu9T3OelJQ==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/iron-webcrypto@0.8.2:
|
/iron-webcrypto@0.8.2:
|
||||||
resolution: {integrity: sha512-jGiwmpgTuF19Vt4hn3+AzaVFGpVZt7A1ysd5ivFel2r4aNVFwqaYa6aU6qsF1PM7b+WFivZHz3nipwUOXaOnHg==}
|
resolution: {integrity: sha512-jGiwmpgTuF19Vt4hn3+AzaVFGpVZt7A1ysd5ivFel2r4aNVFwqaYa6aU6qsF1PM7b+WFivZHz3nipwUOXaOnHg==}
|
||||||
@@ -15092,6 +15088,7 @@ packages:
|
|||||||
dependencies:
|
dependencies:
|
||||||
call-bind: 1.0.2
|
call-bind: 1.0.2
|
||||||
has-tostringtag: 1.0.0
|
has-tostringtag: 1.0.0
|
||||||
|
dev: false
|
||||||
|
|
||||||
/is-array-buffer@3.0.2:
|
/is-array-buffer@3.0.2:
|
||||||
resolution: {integrity: sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==}
|
resolution: {integrity: sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==}
|
||||||
@@ -15197,6 +15194,7 @@ packages:
|
|||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
dependencies:
|
dependencies:
|
||||||
has-tostringtag: 1.0.0
|
has-tostringtag: 1.0.0
|
||||||
|
dev: false
|
||||||
|
|
||||||
/is-glob@4.0.3:
|
/is-glob@4.0.3:
|
||||||
resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
|
resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
|
||||||
@@ -16120,6 +16118,7 @@ packages:
|
|||||||
|
|
||||||
/json-stream@1.0.0:
|
/json-stream@1.0.0:
|
||||||
resolution: {integrity: sha512-H/ZGY0nIAg3QcOwE1QN/rK/Fa7gJn7Ii5obwp6zyPO4xiPNwpIMjqy2gwjBEGqzkF/vSWEIBQCBuN19hYiL6Qg==}
|
resolution: {integrity: sha512-H/ZGY0nIAg3QcOwE1QN/rK/Fa7gJn7Ii5obwp6zyPO4xiPNwpIMjqy2gwjBEGqzkF/vSWEIBQCBuN19hYiL6Qg==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/json5@1.0.2:
|
/json5@1.0.2:
|
||||||
resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==}
|
resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==}
|
||||||
@@ -16761,8 +16760,8 @@ packages:
|
|||||||
/minimist@1.2.8:
|
/minimist@1.2.8:
|
||||||
resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}
|
resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}
|
||||||
|
|
||||||
/minio@7.1.1:
|
/minio@7.1.3:
|
||||||
resolution: {integrity: sha512-HBLRFXs1CkNwAkahU+j1ilB9YS/Tmkdc6orpxVW1YN11NlEJyLjarIpBYu/inF+dj+tJIsA8PSKNnRmUNm+9qQ==}
|
resolution: {integrity: sha512-xPrLjWkTT5E7H7VnzOjF//xBp9I40jYB4aWhb2xTFopXXfw+Wo82DDWngdUju7Doy3Wk7R8C4LAgwhLHHnf0wA==}
|
||||||
engines: {node: ^16 || ^18 || >=20}
|
engines: {node: ^16 || ^18 || >=20}
|
||||||
dependencies:
|
dependencies:
|
||||||
async: 3.2.4
|
async: 3.2.4
|
||||||
@@ -16779,6 +16778,7 @@ packages:
|
|||||||
web-encoding: 1.1.5
|
web-encoding: 1.1.5
|
||||||
xml: 1.0.1
|
xml: 1.0.1
|
||||||
xml2js: 0.5.0
|
xml2js: 0.5.0
|
||||||
|
dev: false
|
||||||
|
|
||||||
/mjml-accordion@4.14.1:
|
/mjml-accordion@4.14.1:
|
||||||
resolution: {integrity: sha512-dpNXyjnhYwhM75JSjD4wFUa9JgHm86M2pa0CoTzdv1zOQz67ilc4BoK5mc2S0gOjJpjBShM5eOJuCyVIuAPC6w==}
|
resolution: {integrity: sha512-dpNXyjnhYwhM75JSjD4wFUa9JgHm86M2pa0CoTzdv1zOQz67ilc4BoK5mc2S0gOjJpjBShM5eOJuCyVIuAPC6w==}
|
||||||
@@ -18867,6 +18867,7 @@ packages:
|
|||||||
filter-obj: 1.1.0
|
filter-obj: 1.1.0
|
||||||
split-on-first: 1.1.0
|
split-on-first: 1.1.0
|
||||||
strict-uri-encode: 2.0.0
|
strict-uri-encode: 2.0.0
|
||||||
|
dev: false
|
||||||
|
|
||||||
/query-string@8.1.0:
|
/query-string@8.1.0:
|
||||||
resolution: {integrity: sha512-BFQeWxJOZxZGix7y+SByG3F36dA0AbTy9o6pSmKFcFz7DAj0re9Frkty3saBn3nHo3D0oZJ/+rx3r8H8r8Jbpw==}
|
resolution: {integrity: sha512-BFQeWxJOZxZGix7y+SByG3F36dA0AbTy9o6pSmKFcFz7DAj0re9Frkty3saBn3nHo3D0oZJ/+rx3r8H8r8Jbpw==}
|
||||||
@@ -19472,6 +19473,7 @@ packages:
|
|||||||
inherits: 2.0.4
|
inherits: 2.0.4
|
||||||
string_decoder: 1.3.0
|
string_decoder: 1.3.0
|
||||||
util-deprecate: 1.0.2
|
util-deprecate: 1.0.2
|
||||||
|
dev: false
|
||||||
|
|
||||||
/readdirp@3.6.0:
|
/readdirp@3.6.0:
|
||||||
resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==}
|
resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==}
|
||||||
@@ -19972,6 +19974,7 @@ packages:
|
|||||||
|
|
||||||
/sax@1.2.4:
|
/sax@1.2.4:
|
||||||
resolution: {integrity: sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==}
|
resolution: {integrity: sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/saxes@6.0.0:
|
/saxes@6.0.0:
|
||||||
resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==}
|
resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==}
|
||||||
@@ -20515,6 +20518,7 @@ packages:
|
|||||||
/split-on-first@1.1.0:
|
/split-on-first@1.1.0:
|
||||||
resolution: {integrity: sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==}
|
resolution: {integrity: sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/split-on-first@3.0.0:
|
/split-on-first@3.0.0:
|
||||||
resolution: {integrity: sha512-qxQJTx2ryR0Dw0ITYyekNQWpz6f8dGd7vffGNflQQ3Iqj9NJ6qiZ7ELpZsJ/QBhIVAiDfXdag3+Gp8RvWa62AA==}
|
resolution: {integrity: sha512-qxQJTx2ryR0Dw0ITYyekNQWpz6f8dGd7vffGNflQQ3Iqj9NJ6qiZ7ELpZsJ/QBhIVAiDfXdag3+Gp8RvWa62AA==}
|
||||||
@@ -20620,6 +20624,7 @@ packages:
|
|||||||
/strict-uri-encode@2.0.0:
|
/strict-uri-encode@2.0.0:
|
||||||
resolution: {integrity: sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ==}
|
resolution: {integrity: sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ==}
|
||||||
engines: {node: '>=4'}
|
engines: {node: '>=4'}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/string-hash@1.1.3:
|
/string-hash@1.1.3:
|
||||||
resolution: {integrity: sha512-kJUvRUFK49aub+a7T1nNE66EJbZBMnBgoC1UbCZ5n6bsZKBRga4KgBRTMn/pFkeCZSYtNeSyMxPDM0AXWELk2A==}
|
resolution: {integrity: sha512-kJUvRUFK49aub+a7T1nNE66EJbZBMnBgoC1UbCZ5n6bsZKBRga4KgBRTMn/pFkeCZSYtNeSyMxPDM0AXWELk2A==}
|
||||||
@@ -20703,6 +20708,7 @@ packages:
|
|||||||
resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==}
|
resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==}
|
||||||
dependencies:
|
dependencies:
|
||||||
safe-buffer: 5.2.1
|
safe-buffer: 5.2.1
|
||||||
|
dev: false
|
||||||
|
|
||||||
/stringify-object@3.3.0:
|
/stringify-object@3.3.0:
|
||||||
resolution: {integrity: sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw==}
|
resolution: {integrity: sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw==}
|
||||||
@@ -20775,6 +20781,7 @@ packages:
|
|||||||
|
|
||||||
/strnum@1.0.5:
|
/strnum@1.0.5:
|
||||||
resolution: {integrity: sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==}
|
resolution: {integrity: sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/style-inject@0.3.0:
|
/style-inject@0.3.0:
|
||||||
resolution: {integrity: sha512-IezA2qp+vcdlhJaVm5SOdPPTUu0FCEqfNSli2vRuSIBbu5Nq5UvygTk/VzeCqfLz2Atj3dVII5QBKGZRZ0edzw==}
|
resolution: {integrity: sha512-IezA2qp+vcdlhJaVm5SOdPPTUu0FCEqfNSli2vRuSIBbu5Nq5UvygTk/VzeCqfLz2Atj3dVII5QBKGZRZ0edzw==}
|
||||||
@@ -21183,6 +21190,7 @@ packages:
|
|||||||
resolution: {integrity: sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==}
|
resolution: {integrity: sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==}
|
||||||
dependencies:
|
dependencies:
|
||||||
readable-stream: 3.6.2
|
readable-stream: 3.6.2
|
||||||
|
dev: false
|
||||||
|
|
||||||
/through@2.3.8:
|
/through@2.3.8:
|
||||||
resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==}
|
resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==}
|
||||||
@@ -22092,6 +22100,7 @@ packages:
|
|||||||
is-generator-function: 1.0.10
|
is-generator-function: 1.0.10
|
||||||
is-typed-array: 1.1.12
|
is-typed-array: 1.1.12
|
||||||
which-typed-array: 1.1.11
|
which-typed-array: 1.1.11
|
||||||
|
dev: false
|
||||||
|
|
||||||
/utila@0.4.0:
|
/utila@0.4.0:
|
||||||
resolution: {integrity: sha512-Z0DbgELS9/L/75wZbro8xAnT50pBVFQZ+hUEueGDU5FN51YSCYM+jdxsfCiHjwNP/4LCDD0i/graKpeBnOXKRA==}
|
resolution: {integrity: sha512-Z0DbgELS9/L/75wZbro8xAnT50pBVFQZ+hUEueGDU5FN51YSCYM+jdxsfCiHjwNP/4LCDD0i/graKpeBnOXKRA==}
|
||||||
@@ -22298,6 +22307,7 @@ packages:
|
|||||||
util: 0.12.5
|
util: 0.12.5
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@zxing/text-encoding': 0.9.0
|
'@zxing/text-encoding': 0.9.0
|
||||||
|
dev: false
|
||||||
|
|
||||||
/web-namespaces@1.1.4:
|
/web-namespaces@1.1.4:
|
||||||
resolution: {integrity: sha512-wYxSGajtmoP4WxfejAPIr4l0fVh+jeMXZb08wNc0tMg6xsfZXj3cECqIK0G7ZAqUq0PP8WlMDtaOGVBTAWztNw==}
|
resolution: {integrity: sha512-wYxSGajtmoP4WxfejAPIr4l0fVh+jeMXZb08wNc0tMg6xsfZXj3cECqIK0G7ZAqUq0PP8WlMDtaOGVBTAWztNw==}
|
||||||
@@ -22696,13 +22706,16 @@ packages:
|
|||||||
dependencies:
|
dependencies:
|
||||||
sax: 1.2.4
|
sax: 1.2.4
|
||||||
xmlbuilder: 11.0.1
|
xmlbuilder: 11.0.1
|
||||||
|
dev: false
|
||||||
|
|
||||||
/xml@1.0.1:
|
/xml@1.0.1:
|
||||||
resolution: {integrity: sha512-huCv9IH9Tcf95zuYCsQraZtWnJvBtLVE0QHMOs8bWyZAFZNDcYjsPq1nEx8jKA9y+Beo9v+7OBPRisQTjinQMw==}
|
resolution: {integrity: sha512-huCv9IH9Tcf95zuYCsQraZtWnJvBtLVE0QHMOs8bWyZAFZNDcYjsPq1nEx8jKA9y+Beo9v+7OBPRisQTjinQMw==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/xmlbuilder@11.0.1:
|
/xmlbuilder@11.0.1:
|
||||||
resolution: {integrity: sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==}
|
resolution: {integrity: sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==}
|
||||||
engines: {node: '>=4.0'}
|
engines: {node: '>=4.0'}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/xmlchars@2.2.0:
|
/xmlchars@2.2.0:
|
||||||
resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==}
|
resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==}
|
||||||
|
|||||||
Reference in New Issue
Block a user