Allow PDF files to be drag and dropped (#31015)

This commit is contained in:
Brian R. Bondy
2025-09-05 13:17:29 -04:00
committed by GitHub
parent 51beb08e05
commit 22ef71463f
5 changed files with 145 additions and 24 deletions
@@ -7,7 +7,7 @@ import * as React from 'react'
import { getLocale } from '$web-common/locale'
import Icon from '@brave/leo/react/icon'
import styles from './style.module.scss'
import { isImageFile } from '../../constants/file_types'
import { isImageFile, isPdfFile } from '../../constants/file_types'
import { useConversation } from '../../state/conversation_context'
import { convertFileToUploadedFile } from '../../utils/file_utils'
@@ -23,7 +23,9 @@ export default function DragOverlay() {
e.stopPropagation()
clearDragState()
const files = Array.from(e.dataTransfer?.files || []).filter(isImageFile)
const files = Array.from(e.dataTransfer?.files || []).filter(file =>
isImageFile(file) || isPdfFile(file)
)
if (files.length === 0) {
return
@@ -3,7 +3,7 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at https://mozilla.org/MPL/2.0/. */
import { isImageFile } from './file_types'
import { isImageFile, isPdfFile } from './file_types'
describe('isImageFile', () => {
// Helper function to create mock File objects
@@ -114,3 +114,45 @@ describe('isImageFile', () => {
})
})
})
describe('isPdfFile', () => {
// Helper function to create mock File objects
const createMockFile = (name: string, type: string): File => {
return new File([''], name, { type })
}
it('accepts PDF files', () => {
const file = createMockFile('test.pdf', 'application/pdf')
expect(isPdfFile(file)).toBe(true)
})
it('handles uppercase MIME type', () => {
const file = createMockFile('test.pdf', 'APPLICATION/PDF')
expect(isPdfFile(file)).toBe(true)
})
it('handles mixed case MIME type', () => {
const file = createMockFile('test.pdf', 'Application/Pdf')
expect(isPdfFile(file)).toBe(true)
})
it('rejects image files', () => {
const file = createMockFile('test.png', 'image/png')
expect(isPdfFile(file)).toBe(false)
})
it('rejects text files', () => {
const file = createMockFile('test.txt', 'text/plain')
expect(isPdfFile(file)).toBe(false)
})
it('rejects empty MIME type', () => {
const file = createMockFile('test', '')
expect(isPdfFile(file)).toBe(false)
})
it('rejects malformed MIME type', () => {
const file = createMockFile('test', 'not-a-mime-type')
expect(isPdfFile(file)).toBe(false)
})
})
@@ -14,3 +14,8 @@ const SUPPORTED_IMAGE_TYPES: string[] = [
export const isImageFile = (file: File): boolean => {
return SUPPORTED_IMAGE_TYPES.includes(file.type.toLowerCase())
}
// Check if file is a PDF
export const isPdfFile = (file: File): boolean => {
return file.type.toLowerCase() === 'application/pdf'
}
@@ -3,7 +3,7 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at https://mozilla.org/MPL/2.0/. */
import { convertFileToUploadedFile, FileReadError, ImageProcessingError } from './file_utils'
import { convertFileToUploadedFile, FileReadError, ImageProcessingError, UnsupportedFileTypeError } from './file_utils'
import * as Mojom from '../../common/mojom'
import getAPI from '../api'
@@ -20,7 +20,6 @@ jest.mock('$web-common/loadTimeData', () => ({
}
}))
describe('convertFileToUploadedFile', () => {
let mockHandler: any
let mockService: any
@@ -97,7 +96,6 @@ describe('convertFileToUploadedFile', () => {
return new File(['mock content'], name, { type, size: size })
}
describe('successful file processing', () => {
it('converts image file successfully', async () => {
const file = createMockFile('test.png', 'image/png', 1500)
@@ -161,6 +159,32 @@ describe('convertFileToUploadedFile', () => {
expect(result).toEqual(mockProcessedFile)
})
it('converts PDF file successfully', async () => {
const file = createMockFile('test.pdf', 'application/pdf', 2000)
const mockArrayBuffer = new ArrayBuffer(8)
const expectedData = Array.from(new Uint8Array(mockArrayBuffer))
// Override readAsArrayBuffer to trigger success
mockFileReader.readAsArrayBuffer.mockImplementation(() => {
process.nextTick(() => {
if (mockFileReader.onload) {
mockFileReader.onload({ target: { result: mockArrayBuffer } })
}
})
})
const result = await convertFileToUploadedFile(file)
expect(mockFileReader.readAsArrayBuffer).toHaveBeenCalledWith(file)
expect(mockHandler.processImageFile).not.toHaveBeenCalled()
expect(result).toEqual({
filename: 'test.pdf',
filesize: file.size, // Use actual file size
data: expectedData,
type: Mojom.UploadedFileType.kPdf
})
})
it('throws ImageProcessingError for empty files', async () => {
const file = createMockFile('empty.png', 'image/png', 0)
const mockArrayBuffer = new ArrayBuffer(0)
@@ -258,6 +282,30 @@ describe('convertFileToUploadedFile', () => {
)
})
it('throws UnsupportedFileTypeError for unknown file types', async () => {
const file = createMockFile('test.txt', 'text/plain')
const mockArrayBuffer = new ArrayBuffer(8)
// Override readAsArrayBuffer to trigger success
// (FileReader will work, but file type check will fail)
mockFileReader.readAsArrayBuffer.mockImplementation(() => {
process.nextTick(() => {
if (mockFileReader.onload) {
mockFileReader.onload({ target: { result: mockArrayBuffer } })
}
})
})
await expect(convertFileToUploadedFile(file)).rejects.toThrow(
UnsupportedFileTypeError
)
await expect(convertFileToUploadedFile(file)).rejects.toThrow(
'Unsupported file type: text/plain. Only images and PDF files are ' +
'supported.'
)
expect(mockHandler.processImageFile).not.toHaveBeenCalled()
})
})
describe('data transformation', () => {
@@ -21,12 +21,17 @@ export class ImageProcessingError extends Error {
}
}
export class UnsupportedFileTypeError extends Error {
constructor(message: string) {
super(message)
this.name = 'UnsupportedFileTypeError'
}
}
// Utility function to convert File objects to UploadedFile format
export const convertFileToUploadedFile = async (
file: File
): Promise<Mojom.UploadedFile> => {
// Use backend processing for images via mojo call
const reader = new FileReader()
const arrayBuffer = await new Promise<ArrayBuffer>((resolve, reject) => {
reader.onload = (e) => {
@@ -44,26 +49,45 @@ export const convertFileToUploadedFile = async (
const uint8Array = new Uint8Array(arrayBuffer)
try {
const api = getAPI()
const response = await api.uiHandler.processImageFile(
Array.from(uint8Array),
file.name
)
if (!response.processedFile) {
throw new ImageProcessingError(
'Failed to process image file: Backend returned no result'
)
// Check file type and handle accordingly
const mimeType = file.type.toLowerCase()
if (mimeType === 'application/pdf') {
// Handle PDF files directly
const uploadedFile: Mojom.UploadedFile = {
filename: file.name,
filesize: file.size,
data: Array.from(uint8Array),
type: Mojom.UploadedFileType.kPdf
}
return uploadedFile
} else if (mimeType.startsWith('image/')) {
// Use backend processing for images via mojo call
try {
const api = getAPI()
const response = await api.uiHandler.processImageFile(
Array.from(uint8Array),
file.name
)
return response.processedFile
} catch (error) {
if (error instanceof ImageProcessingError) {
if (!response.processedFile) {
throw new ImageProcessingError(
'Failed to process image file: Backend returned no result'
)
}
return response.processedFile
} catch (error) {
if (error instanceof ImageProcessingError) {
throw error
}
// Re-throw any other errors as-is
throw error
}
// Re-throw any other errors as-is
throw error
} else {
throw new UnsupportedFileTypeError(
`Unsupported file type: ${file.type}. Only images and PDF files ` +
`are supported.`
)
}
}