feat(bot): ⚡️ Add custom file upload size limit
This commit is contained in:
@@ -53,6 +53,7 @@ export const InputChatBlock = ({
|
||||
groupId: block.groupId,
|
||||
content: value,
|
||||
variableId: variableId ?? null,
|
||||
uploadedFiles: block.type === InputBlockType.FILE,
|
||||
})
|
||||
if (!isEditting) onTransitionEnd({ label, value, itemId }, isRetry)
|
||||
setIsEditting(false)
|
||||
|
||||
@@ -11,15 +11,17 @@ type Props = {
|
||||
onSubmit: (url: InputSubmitContent) => void
|
||||
}
|
||||
|
||||
const tenMB = 10 * 1024 * 1024
|
||||
export const FileUploadForm = ({
|
||||
block: {
|
||||
id,
|
||||
options: { isMultipleAllowed, labels },
|
||||
options: { isMultipleAllowed, labels, sizeLimit },
|
||||
},
|
||||
onSubmit,
|
||||
}: Props) => {
|
||||
const { isPreview } = useTypebot()
|
||||
const {
|
||||
isPreview,
|
||||
typebot: { typebotId },
|
||||
} = useTypebot()
|
||||
const { resultId } = useAnswers()
|
||||
const [selectedFiles, setSelectedFiles] = useState<File[]>([])
|
||||
const [isUploading, setIsUploading] = useState(false)
|
||||
@@ -35,8 +37,8 @@ export const FileUploadForm = ({
|
||||
const onNewFiles = (files: FileList) => {
|
||||
setErrorMessage(undefined)
|
||||
const newFiles = Array.from(files)
|
||||
if (newFiles.some((file) => file.size > tenMB))
|
||||
return setErrorMessage('A file is larger than 10MB')
|
||||
if (newFiles.some((file) => file.size > (sizeLimit ?? 10) * 1024 * 1024))
|
||||
return setErrorMessage(`A file is larger than ${sizeLimit ?? 10}MB`)
|
||||
if (!isMultipleAllowed && files) return startSingleFileUpload(newFiles[0])
|
||||
setSelectedFiles([...selectedFiles, ...newFiles])
|
||||
}
|
||||
@@ -55,6 +57,7 @@ export const FileUploadForm = ({
|
||||
})
|
||||
setIsUploading(true)
|
||||
const urls = await uploadFiles({
|
||||
basePath: `/api/typebots/${typebotId}/blocks/${id}`,
|
||||
files: [
|
||||
{
|
||||
file,
|
||||
@@ -76,6 +79,7 @@ export const FileUploadForm = ({
|
||||
})
|
||||
setIsUploading(true)
|
||||
const urls = await uploadFiles({
|
||||
basePath: `/api/typebots/${typebotId}/blocks/${id}`,
|
||||
files: files.map((file) => ({
|
||||
file: file,
|
||||
path: `public/results/${resultId}/${id}/${file.name}`,
|
||||
|
||||
@@ -33,7 +33,7 @@ export type TypebotViewerProps = {
|
||||
startGroupId?: string
|
||||
isLoading?: boolean
|
||||
onNewGroupVisible?: (edge: Edge) => void
|
||||
onNewAnswer?: (answer: Answer) => Promise<void>
|
||||
onNewAnswer?: (answer: Answer & { uploadedFiles: boolean }) => Promise<void>
|
||||
onNewLog?: (log: Omit<Log, 'id' | 'createdAt' | 'resultId'>) => void
|
||||
onCompleted?: () => void
|
||||
onVariablesUpdated?: (variables: VariableWithValue[]) => void
|
||||
@@ -64,7 +64,8 @@ export const TypebotViewer = ({
|
||||
const handleNewGroupVisible = (edge: Edge) =>
|
||||
onNewGroupVisible && onNewGroupVisible(edge)
|
||||
|
||||
const handleNewAnswer = (answer: Answer) => onNewAnswer && onNewAnswer(answer)
|
||||
const handleNewAnswer = (answer: Answer & { uploadedFiles: boolean }) =>
|
||||
onNewAnswer && onNewAnswer(answer)
|
||||
|
||||
const handleNewLog = (log: Omit<Log, 'id' | 'createdAt' | 'resultId'>) =>
|
||||
onNewLog && onNewLog(log)
|
||||
|
||||
@@ -4,7 +4,9 @@ import React, { createContext, ReactNode, useContext, useState } from 'react'
|
||||
const answersContext = createContext<{
|
||||
resultId?: string
|
||||
resultValues: ResultValues
|
||||
addAnswer: (answer: Answer) => Promise<void> | undefined
|
||||
addAnswer: (
|
||||
answer: Answer & { uploadedFiles: boolean }
|
||||
) => Promise<void> | undefined
|
||||
updateVariables: (variables: VariableWithValue[]) => void
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
//@ts-ignore
|
||||
@@ -17,7 +19,9 @@ export const AnswersContext = ({
|
||||
onVariablesUpdated,
|
||||
}: {
|
||||
resultId?: string
|
||||
onNewAnswer: (answer: Answer) => Promise<void> | undefined
|
||||
onNewAnswer: (
|
||||
answer: Answer & { uploadedFiles: boolean }
|
||||
) => Promise<void> | undefined
|
||||
onVariablesUpdated?: (variables: VariableWithValue[]) => void
|
||||
children: ReactNode
|
||||
}) => {
|
||||
@@ -27,7 +31,7 @@ export const AnswersContext = ({
|
||||
createdAt: new Date().toISOString(),
|
||||
})
|
||||
|
||||
const addAnswer = (answer: Answer) => {
|
||||
const addAnswer = (answer: Answer & { uploadedFiles: boolean }) => {
|
||||
setResultValues((resultValues) => ({
|
||||
...resultValues,
|
||||
answers: [...resultValues.answers, answer],
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- Made the column `groups` on table `PublicTypebot` required. This step will fail if there are existing NULL values in that column.
|
||||
- Made the column `edges` on table `PublicTypebot` required. This step will fail if there are existing NULL values in that column.
|
||||
- Made the column `groups` on table `Typebot` required. This step will fail if there are existing NULL values in that column.
|
||||
- Made the column `edges` on table `Typebot` required. This step will fail if there are existing NULL values in that column.
|
||||
|
||||
*/
|
||||
-- AlterTable
|
||||
ALTER TABLE "Answer" ADD COLUMN "storageUsed" INTEGER;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "PublicTypebot" ALTER COLUMN "groups" SET NOT NULL,
|
||||
ALTER COLUMN "edges" SET NOT NULL;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "Result" ADD COLUMN "hasStarted" BOOLEAN;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "Typebot" ALTER COLUMN "groups" SET NOT NULL,
|
||||
ALTER COLUMN "edges" SET NOT NULL;
|
||||
@@ -112,15 +112,15 @@ enum GraphNavigation {
|
||||
}
|
||||
|
||||
model CustomDomain {
|
||||
name String @id
|
||||
createdAt DateTime @default(now())
|
||||
name String @id
|
||||
createdAt DateTime @default(now())
|
||||
workspaceId String
|
||||
workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
|
||||
}
|
||||
|
||||
model Credentials {
|
||||
id String @id @default(cuid())
|
||||
createdAt DateTime @default(now())
|
||||
id String @id @default(cuid())
|
||||
createdAt DateTime @default(now())
|
||||
workspaceId String
|
||||
workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
|
||||
data String // Encrypted data
|
||||
@@ -155,7 +155,7 @@ model DashboardFolder {
|
||||
childrenFolder DashboardFolder[] @relation("ParentChild")
|
||||
typebots Typebot[]
|
||||
workspaceId String
|
||||
workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
|
||||
workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
|
||||
}
|
||||
|
||||
model Typebot {
|
||||
@@ -180,7 +180,7 @@ model Typebot {
|
||||
invitations Invitation[]
|
||||
webhooks Webhook[]
|
||||
workspaceId String
|
||||
workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
|
||||
workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
|
||||
}
|
||||
|
||||
model Invitation {
|
||||
@@ -210,16 +210,16 @@ enum CollaborationType {
|
||||
}
|
||||
|
||||
model PublicTypebot {
|
||||
id String @id @default(cuid())
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @default(now()) @updatedAt
|
||||
typebotId String @unique
|
||||
typebot Typebot @relation(fields: [typebotId], references: [id], onDelete: Cascade)
|
||||
groups Json
|
||||
variables Json[]
|
||||
edges Json
|
||||
theme Json
|
||||
settings Json
|
||||
id String @id @default(cuid())
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @default(now()) @updatedAt
|
||||
typebotId String @unique
|
||||
typebot Typebot @relation(fields: [typebotId], references: [id], onDelete: Cascade)
|
||||
groups Json
|
||||
variables Json[]
|
||||
edges Json
|
||||
theme Json
|
||||
settings Json
|
||||
}
|
||||
|
||||
model Result {
|
||||
@@ -231,6 +231,7 @@ model Result {
|
||||
answers Answer[]
|
||||
variables Json[]
|
||||
isCompleted Boolean
|
||||
hasStarted Boolean?
|
||||
logs Log[]
|
||||
}
|
||||
|
||||
@@ -248,10 +249,11 @@ model Answer {
|
||||
createdAt DateTime @default(now())
|
||||
resultId String
|
||||
result Result @relation(fields: [resultId], references: [id], onDelete: Cascade)
|
||||
blockId String
|
||||
blockId String
|
||||
groupId String
|
||||
variableId String?
|
||||
content String
|
||||
storageUsed Int?
|
||||
|
||||
@@unique([resultId, blockId, groupId])
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { Answer as AnswerFromPrisma } from 'db'
|
||||
|
||||
export type Answer = Omit<AnswerFromPrisma, 'resultId' | 'createdAt'>
|
||||
export type Answer = Omit<
|
||||
AnswerFromPrisma,
|
||||
'resultId' | 'createdAt' | 'storageUsed'
|
||||
> & { storageUsed?: number }
|
||||
|
||||
export type Stats = {
|
||||
totalViews: number
|
||||
|
||||
@@ -8,6 +8,7 @@ export const fileInputOptionsSchema = optionBaseSchema.and(
|
||||
placeholder: z.string(),
|
||||
button: z.string(),
|
||||
}),
|
||||
sizeLimit: z.number().optional(),
|
||||
})
|
||||
)
|
||||
|
||||
|
||||
@@ -3,14 +3,16 @@ import { config, Endpoint, S3 } from 'aws-sdk'
|
||||
type GeneratePresignedUrlProps = {
|
||||
filePath: string
|
||||
fileType: string
|
||||
sizeLimit?: number
|
||||
}
|
||||
|
||||
const tenMB = 10485760
|
||||
const tenMB = 10 * 1024 * 1024
|
||||
const oneHundredAndTwentySeconds = 120
|
||||
|
||||
export const generatePresignedUrl = ({
|
||||
filePath,
|
||||
fileType,
|
||||
sizeLimit = tenMB,
|
||||
}: GeneratePresignedUrlProps): S3.PresignedPost => {
|
||||
if (
|
||||
!process.env.S3_ENDPOINT ||
|
||||
@@ -45,7 +47,7 @@ export const generatePresignedUrl = ({
|
||||
'Content-Type': fileType,
|
||||
},
|
||||
Expires: oneHundredAndTwentySeconds,
|
||||
Conditions: [['content-length-range', 0, tenMB]],
|
||||
Conditions: [['content-length-range', 0, sizeLimit]],
|
||||
})
|
||||
return presignedUrl
|
||||
}
|
||||
|
||||
@@ -193,6 +193,7 @@ export const generateId = (idDesiredLength: number): string => {
|
||||
}
|
||||
|
||||
type UploadFileProps = {
|
||||
basePath?: string
|
||||
files: {
|
||||
file: File
|
||||
path: string
|
||||
@@ -202,6 +203,7 @@ type UploadFileProps = {
|
||||
type UrlList = string[]
|
||||
|
||||
export const uploadFiles = async ({
|
||||
basePath = '/api',
|
||||
files,
|
||||
onUploadProgress,
|
||||
}: UploadFileProps): Promise<UrlList> => {
|
||||
@@ -209,9 +211,9 @@ export const uploadFiles = async ({
|
||||
const { data } = await sendRequest<{
|
||||
presignedUrl: { url: string; fields: any }
|
||||
}>(
|
||||
`/api/storage/upload-url?filePath=${encodeURIComponent(path)}&fileType=${
|
||||
file.type
|
||||
}`
|
||||
`${basePath}/storage/upload-url?filePath=${encodeURIComponent(
|
||||
path
|
||||
)}&fileType=${file.type}`
|
||||
)
|
||||
|
||||
if (!data?.presignedUrl) return null
|
||||
|
||||
Reference in New Issue
Block a user