-
Notifications
You must be signed in to change notification settings - Fork 305
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(dropzone): add dropzone support for files within dropped directo…
…ries (#6062) * feat(dropzone): allow dropzone to handle directory contents and multiple files * chore: update usage of onDrop to reflect it is now async * chore: make onDrop synchronous, move traversal into util, add util tests * chore: rearrange onDrop logic for better readability * chore: clean up unnecessary typing * chore: bumping size limit for components which use dropzone * chore(tests): add testing for directories and mixed files/directories * chore: rename util, add destructuring, simplify logic * chore: simplify logic in onDrop * chore: remove unnecessary return * chore: destructure data transfer item * chore: converting ternary to if/else
- Loading branch information
Showing
4 changed files
with
223 additions
and
9 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
141 changes: 141 additions & 0 deletions
141
packages/react-core/src/utils/__tests__/processDroppedEntries.spec.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,141 @@ | ||
import { processDroppedItems } from '../processDroppedItems'; | ||
|
||
describe('processDroppedItems', () => { | ||
const mockFileData = new Blob(['test content'], { type: 'text/plain' }); | ||
const mockFile = new File([mockFileData], 'test.txt', { type: 'text/plain' }); | ||
|
||
const createMockFileEntry = (file: File): FileSystemFileEntry => ({ | ||
isFile: true, | ||
isDirectory: false, | ||
name: file.name, | ||
fullPath: `/${file.name}`, | ||
filesystem: { | ||
name: 'temporary', | ||
root: {} as FileSystemDirectoryEntry, | ||
}, | ||
file: (callback: (file: File) => void) => callback(file), | ||
getParent: jest.fn(), | ||
}); | ||
|
||
const createMockDirectoryEntry = ( | ||
files: File[] | ||
): FileSystemDirectoryEntry => { | ||
const entries = files.map((file) => createMockFileEntry(file)); | ||
let firstCall = true; | ||
return { | ||
getDirectory: jest.fn(), | ||
getFile: jest.fn(), | ||
isFile: false, | ||
isDirectory: true, | ||
name: 'test-directory', | ||
fullPath: '/test-directory', | ||
filesystem: { | ||
name: 'temporary', | ||
root: {} as FileSystemDirectoryEntry, | ||
}, | ||
createReader: () => ({ | ||
readEntries: (resolve) => { | ||
if (firstCall) { | ||
firstCall = false; | ||
resolve(entries); | ||
} else { | ||
resolve([]); | ||
} | ||
}, | ||
}), | ||
getParent: jest.fn(), | ||
}; | ||
}; | ||
|
||
const createMockDataTransferItem = ( | ||
entry: FileSystemEntry | ||
): DataTransferItem => ({ | ||
getAsFile: jest.fn(), | ||
getAsString: jest.fn(), | ||
kind: 'file', | ||
type: 'text/plain', | ||
webkitGetAsEntry: () => entry, | ||
}); | ||
|
||
it('should process a single file', async () => { | ||
const fileEntry = createMockFileEntry(mockFile); | ||
const items = [createMockDataTransferItem(fileEntry)]; | ||
|
||
const result = await processDroppedItems(items); | ||
|
||
expect(result).toHaveLength(1); | ||
expect(result[0].name).toBe('test.txt'); | ||
expect(result[0].type).toBe('text/plain'); | ||
}); | ||
|
||
it('should process multiple files', async () => { | ||
const file1 = new File([mockFileData], 'test1.txt', { type: 'text/plain' }); | ||
const file2 = new File([mockFileData], 'test2.txt', { type: 'text/plain' }); | ||
const items = [ | ||
createMockDataTransferItem(createMockFileEntry(file1)), | ||
createMockDataTransferItem(createMockFileEntry(file2)), | ||
]; | ||
|
||
const result = await processDroppedItems(items); | ||
|
||
expect(result).toHaveLength(2); | ||
expect(result[0].name).toBe('test1.txt'); | ||
expect(result[1].name).toBe('test2.txt'); | ||
}); | ||
|
||
it('should process files in a directory', async () => { | ||
const filesInDir = [ | ||
new File([mockFileData], 'dir-file1.txt', { type: 'text/plain' }), | ||
new File([mockFileData], 'dir-file2.txt', { type: 'text/plain' }), | ||
]; | ||
const dirEntry = createMockDirectoryEntry(filesInDir); | ||
const items = [createMockDataTransferItem(dirEntry)]; | ||
|
||
const result = await processDroppedItems(items); | ||
|
||
expect(result).toHaveLength(2); | ||
expect(result[0].name).toBe('dir-file1.txt'); | ||
expect(result[1].name).toBe('dir-file2.txt'); | ||
}); | ||
|
||
it('should process mixed files and directories', async () => { | ||
const singleFile = new File([mockFileData], 'single.txt', { | ||
type: 'text/plain', | ||
}); | ||
const filesInDir = [ | ||
new File([mockFileData], 'dir-file1.txt', { type: 'text/plain' }), | ||
new File([mockFileData], 'dir-file2.txt', { type: 'text/plain' }), | ||
]; | ||
const items = [ | ||
createMockDataTransferItem(createMockFileEntry(singleFile)), | ||
createMockDataTransferItem(createMockDirectoryEntry(filesInDir)), | ||
]; | ||
|
||
const result = await processDroppedItems(items); | ||
|
||
expect(result).toHaveLength(3); | ||
expect(result.map((f) => f.name)).toContain('single.txt'); | ||
expect(result.map((f) => f.name)).toContain('dir-file1.txt'); | ||
expect(result.map((f) => f.name)).toContain('dir-file2.txt'); | ||
}); | ||
|
||
it('should handle empty items array', async () => { | ||
const result = await processDroppedItems([]); | ||
|
||
expect(result).toHaveLength(0); | ||
}); | ||
|
||
it('should handle non-file items', async () => { | ||
const items = [ | ||
{ | ||
kind: 'string', | ||
type: 'text/plain', | ||
webkitGetAsEntry: () => null, | ||
}, | ||
] as DataTransferItem[]; | ||
|
||
const result = await processDroppedItems(items); | ||
|
||
expect(result).toHaveLength(0); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
// Helper function to convert FileSystemFileEntry to File | ||
const getFileFromEntry = (fileEntry: FileSystemFileEntry): Promise<File> => { | ||
return new Promise((resolve) => { | ||
fileEntry.file(resolve); | ||
}); | ||
}; | ||
|
||
// Helper function to read all entries in a directory | ||
const readAllDirectoryEntries = async ( | ||
dirReader: FileSystemDirectoryReader | ||
): Promise<FileSystemEntry[]> => { | ||
const entries: FileSystemEntry[] = []; | ||
|
||
let readBatch: FileSystemEntry[] = []; | ||
do { | ||
readBatch = await new Promise<FileSystemEntry[]>((resolve, reject) => { | ||
try { | ||
dirReader.readEntries(resolve, reject); | ||
} catch (error) { | ||
reject(error); | ||
} | ||
}); | ||
entries.push(...readBatch); | ||
} while (readBatch.length > 0); | ||
|
||
return entries; | ||
}; | ||
|
||
// Helper function to process files and folder contents | ||
export async function processDroppedItems( | ||
dataTransferItems: DataTransferItem[] | ||
): Promise<File[]> { | ||
const files: File[] = []; | ||
|
||
const processFileSystemEntry = async ( | ||
entry: FileSystemEntry | ||
): Promise<void> => { | ||
if (entry.isFile) { | ||
const file = await getFileFromEntry(entry as FileSystemFileEntry); | ||
files.push(file); | ||
} else if (entry.isDirectory) { | ||
const dirReader = (entry as FileSystemDirectoryEntry).createReader(); | ||
const dirEntries = await readAllDirectoryEntries(dirReader); | ||
await Promise.all(dirEntries.map(processFileSystemEntry)); | ||
} | ||
}; | ||
|
||
// Filter out and process files from the data transfer items | ||
await Promise.all( | ||
dataTransferItems | ||
.reduce<FileSystemEntry[]>( | ||
(acc, { kind, webkitGetAsEntry }) => | ||
kind === 'file' && webkitGetAsEntry() | ||
? [...acc, webkitGetAsEntry()!] | ||
: acc, | ||
[] | ||
) | ||
.map(processFileSystemEntry) | ||
); | ||
|
||
return files; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters