From f5b90bf999affab1e3ed848e57435139f05e6af5 Mon Sep 17 00:00:00 2001 From: Alex Kern Date: Wed, 16 Jul 2025 20:40:59 -0700 Subject: [PATCH] feat(upload): allow adding more files before starting --- src/app/page.tsx | 11 ++++++++ src/components/AddFilesButton.tsx | 45 +++++++++++++++++++++++++++++++ tests/e2e/add-files.test.ts | 17 ++++++++++++ tests/e2e/helpers.ts | 27 +++++++++++++++++++ 4 files changed, 100 insertions(+) create mode 100644 src/components/AddFilesButton.tsx create mode 100644 tests/e2e/add-files.test.ts diff --git a/src/app/page.tsx b/src/app/page.tsx index f8d06d2..cff189b 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -16,6 +16,7 @@ import { getFileName } from '../fs' import TitleText from '../components/TitleText' import { pluralize } from '../utils/pluralize' import TermsAcceptance from '../components/TermsAcceptance' +import AddFilesButton from '../components/AddFilesButton' function PageWrapper({ children }: { children: React.ReactNode }): JSX.Element { return ( @@ -59,6 +60,7 @@ function ConfirmUploadState({ onCancel, onStart, onRemoveFile, + onAddFiles, }: { uploadedFiles: UploadedFile[] password: string @@ -66,6 +68,7 @@ function ConfirmUploadState({ onCancel: () => void onStart: () => void onRemoveFile: (index: number) => void + onAddFiles: (files: UploadedFile[]) => void }): JSX.Element { const fileListData = useUploaderFileListData(uploadedFiles) return ( @@ -75,6 +78,9 @@ function ConfirmUploadState({ {pluralize(uploadedFiles.length, 'file', 'files')}. +
+ +
@@ -137,6 +143,10 @@ export default function UploadPage(): JSX.Element { setUploadedFiles((fs) => fs.filter((_, i) => i !== index)) }, []) + const handleAddFiles = useCallback((files: UploadedFile[]) => { + setUploadedFiles((fs) => [...fs, ...files]) + }, []) + if (!uploadedFiles.length) { return } @@ -150,6 +160,7 @@ export default function UploadPage(): JSX.Element { onCancel={handleCancel} onStart={handleStart} onRemoveFile={handleRemoveFile} + onAddFiles={handleAddFiles} /> ) } diff --git a/src/components/AddFilesButton.tsx b/src/components/AddFilesButton.tsx new file mode 100644 index 0000000..1a11526 --- /dev/null +++ b/src/components/AddFilesButton.tsx @@ -0,0 +1,45 @@ +import React, { useRef, useCallback, JSX } from 'react' +import { UploadedFile } from '../types' + +export default function AddFilesButton({ + onAdd, +}: { + onAdd: (files: UploadedFile[]) => void +}): JSX.Element { + const fileInputRef = useRef(null) + + const handleClick = useCallback(() => { + fileInputRef.current?.click() + }, []) + + const handleChange = useCallback( + (e: React.ChangeEvent) => { + if (e.target.files) { + onAdd(Array.from(e.target.files) as UploadedFile[]) + e.target.value = '' + } + }, + [onAdd], + ) + + return ( + <> + + + + ) +} diff --git a/tests/e2e/add-files.test.ts b/tests/e2e/add-files.test.ts new file mode 100644 index 0000000..17da139 --- /dev/null +++ b/tests/e2e/add-files.test.ts @@ -0,0 +1,17 @@ +/// +import { test, expect } from '@playwright/test' +import { createTestFile, uploadFile, addFile } from './helpers' + +test('user can add more files before starting upload', async ({ page }) => { + const file1 = createTestFile('first.txt', 'A') + const file2 = createTestFile('second.txt', 'B') + + await uploadFile(page, file1) + + // Add another file using the add files button + await addFile(page, file2) + + // Both files should be listed + await expect(page.getByText(file1.name)).toBeVisible() + await expect(page.getByText(file2.name)).toBeVisible() +}) diff --git a/tests/e2e/helpers.ts b/tests/e2e/helpers.ts index 48ea67d..a47b7d3 100644 --- a/tests/e2e/helpers.ts +++ b/tests/e2e/helpers.ts @@ -66,6 +66,33 @@ export async function uploadFile( ) } +export async function addFile( + page: Page, + testFile: TestFile, +): Promise { + await page.evaluate( + ({ testContent, testFileName }) => { + const input = document.querySelector( + '#add-files-input', + ) as HTMLInputElement + if (input) { + const file = new File([testContent], testFileName, { + type: 'text/plain', + }) + const dataTransfer = new DataTransfer() + dataTransfer.items.add(file) + input.files = dataTransfer.files + + const event = new Event('change', { bubbles: true }) + input.dispatchEvent(event) + } + }, + { testContent: testFile.content, testFileName: testFile.name }, + ) + + await expect(page.getByText(testFile.name)).toBeVisible({ timeout: 5000 }) +} + export async function startUpload(page: Page): Promise { // Start sharing await page.locator('#start-button').click()