# UploadInputV3 Duplicate File / Max Files Reached After Upload Fix Plan

Created: 2026-04-23
Author: smarcet@gmail.com
Status: VERIFIED
Approved: Yes
Iterations: 0
Worktree: No
Type: Bugfix

## Summary

**Symptom:** After uploading a file via UploadInputV3, the component shows "Maximum number of files has been reached" error and displays the file twice — once with the original filename (`image.png`) and once with the server-renamed/hashed filename (`image_e2eed7fb3bc6bd1b16f77beb0f375b82.png`).
**Trigger:** Upload a single file when `maxFiles=1`. The server renames the file (adds hash). The component fails to clean up the transitional `uploadingFiles` entry because the renamed filename doesn't match.
**Root Cause:** `src/components/inputs/upload-input-v3/index.js:164` — The `useEffect` cleanup compares `v.filename === f.name` to match completed `uploadingFiles` entries against `value` entries. When the server renames the file, this comparison fails, the stale completed entry persists, and the file appears twice.

**Secondary issue:** `src/components/inputs/dropzone/index.js:156` — Missing `return` after `done('Max files reached.')` causes `done()` to be called a second time (without error), potentially accepting files that should be rejected.

## Investigation

- **V3-specific bug.** V2 does not maintain `uploadingFiles` state — it relies on Dropzone's built-in preview and only renders from the `value` prop. V3 introduced `uploadingFiles` state for custom progress UI, with a `useEffect` that cleans up completed entries when `value` updates. This cleanup fails on server-renamed files.
- **Flow:** `handleAddedFile` → `handleFileCompleted` (marks `complete:true` with original name) → parent updates `value` (with server-renamed filename) → `useEffect` compares `v.filename === f.name` → mismatch → stale entry persists.
- **The `accept` callback** at `dropzone/index.js:155-159` has a missing `return` after rejecting with `done('Max files reached.')`, causing a fallthrough to `done()`.

## Behavior Contract

**Given:** UploadInputV3 with `maxFiles=1`, a file is uploaded and the server renames it (response contains a different filename than the original)
**When:** The parent updates the `value` prop with the server-returned file entry (containing the renamed filename)
**Currently (bug):** The completed `uploadingFiles` entry (with original name) persists because `v.filename !== f.name`, causing the file to appear twice — once from `uploadingFiles` and once from `value`
**Expected (fix):** When `value` updates with new entries, all completed `uploadingFiles` entries are removed regardless of filename, since `value` is the source of truth for uploaded files. The file appears exactly once (from `value`).
**Anti-regression:** Files still uploading (not yet complete) must NOT be removed from `uploadingFiles` when `value` changes. Error file display, progress tracking, and file deletion functionality must remain intact.

## Fix Approach

**Chosen:** Remove all completed entries on value change
**Why:** The `value` prop is the authoritative source of truth for uploaded files. Once it changes to include new entries, any completed `uploadingFiles` entries are transitional and redundant — they exist only to bridge the visual gap between upload completion and parent state update. Removing all completed entries when `value` has entries is simple, handles server renames, and is semantically correct.
**Alternatives considered:**
- *Track server name through upload flow* — precise but complex; requires heuristic matching in `wrappedOnUploadComplete` to find which `uploadingFiles` entry corresponds to the response. Fragile for concurrent uploads.
- *Fuzzy filename matching* — check if value entry's filename contains the base name; fragile, depends on server naming convention.

**Files:**
- `src/components/inputs/upload-input-v3/index.js` (primary fix — useEffect cleanup)
- `src/components/inputs/dropzone/index.js` (secondary fix — missing return)
- `src/components/inputs/upload-input-v3/__tests__/upload-input-v3.test.js` (reproducing test)

**Strategy:** Change the `useEffect` at line 160-166 to remove all completed entries from `uploadingFiles` when `value` has entries, instead of matching by exact filename. Add missing `return` after `done('Max files reached.')` in the `accept` callback.

**Tests:** Add test to `upload-input-v3.test.js` that simulates: file added → file completed → value updated with different filename → verify only one file entry visible (from value, not from uploadingFiles).

## Verification Scenario

### TS-001: Server-Renamed File Upload
**Preconditions:** UploadInputV3 with maxFiles=1, server configured to rename uploaded files

| Step | Action | Expected Result (after fix) |
|------|--------|-----------------------------|
| 1 | Upload a single file via UploadInputV3 | File appears as "Loading" then "Complete" |
| 2 | Parent updates value with server-renamed file | File appears exactly once (from value), no duplicate entry, no "max files" error |

## Progress

- [x] Task 1: Write Reproducing Test (RED)
- [x] Task 2: Implement Fix at Root Cause
- [x] Task 3: Quality Gate
      **Tasks:** 3 | **Done:** 3

## Tasks

### Task 1: Write Reproducing Test (RED)

**Objective:** Encode the Behavior Contract as a failing test BEFORE writing any fix code.
**Files:** `src/components/inputs/upload-input-v3/__tests__/upload-input-v3.test.js`
**Entry point:** `UploadInputV3` component (rendered with mock DropzoneV3)
**Test scenario:**
1. Render UploadInputV3 with `maxFiles=1` and `value=[]`
2. Simulate `onAddedFile({ name: 'image.png', size: 75000 })`
3. Simulate `onFileCompleted({ name: 'image.png', size: 75000 })`
4. Re-render with `value=[{ filename: 'image_abc123.png', size: 75000 }]` (server-renamed)
5. Assert: only ONE file entry with text `image_abc123.png` is visible; original `image.png` is NOT visible
**DoD:** Test exists, named `test('cleans up completed uploading file when value updates with server-renamed filename')`, runs, fails because the stale `image.png` entry persists alongside `image_abc123.png`.
**Verify:** `npx jest src/components/inputs/upload-input-v3/__tests__/upload-input-v3.test.js --verbose`

### Task 2: Implement Fix at Root Cause

**Objective:** Minimal change to fix the useEffect cleanup and the missing return.
**Files:**
- `src/components/inputs/upload-input-v3/index.js` — change useEffect at line 160-166 to remove all completed entries when value has entries
- `src/components/inputs/dropzone/index.js` — add `return` after `done('Max files reached.')` at line 156
**Strategy:**
1. In `upload-input-v3/index.js`, change the useEffect cleanup from:
   ```javascript
   setUploadingFiles(prev => prev.filter(f => {
     if (!f.complete) return true;
     return !value.some(v => v.filename === f.name);
   }));
   ```
   To:
   ```javascript
   setUploadingFiles(prev => prev.filter(f => !f.complete));
   ```
2. In `dropzone/index.js`, add `return;` after `done('Max files reached.');` at line 156.
**DoD:** Reproducing test PASSES. Full test suite PASSES. Diff touches root-cause files.
**Verify:** `npx jest --verbose`

### Task 3: Quality Gate

**Objective:** Lint + build clean, full suite re-run.
**DoD:** Full suite green, no lint errors, build succeeds.
**Verify:** `npx jest --verbose && npm run build-dev`
