openstack-uicore-foundation 5.0.14 → 5.0.15-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs/plans/2026-04-23-uploadv3-duplicate-file-max-reached.md +109 -0
- package/lib/components/index.js +1 -1
- package/lib/components/index.js.map +1 -1
- package/lib/components/inputs/upload-input-v2.js +1 -1
- package/lib/components/inputs/upload-input-v2.js.map +1 -1
- package/lib/components/inputs/upload-input-v3.js +1 -1
- package/lib/components/inputs/upload-input-v3.js.map +1 -1
- package/lib/components/mui/formik-inputs/upload.js +1 -1
- package/lib/components/mui/formik-inputs/upload.js.map +1 -1
- package/lib/components/mui/upload-dialog.js +1 -1
- package/lib/components/mui/upload-dialog.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# UploadInputV3 Duplicate File / Max Files Reached After Upload Fix Plan
|
|
2
|
+
|
|
3
|
+
Created: 2026-04-23
|
|
4
|
+
Author: smarcet@gmail.com
|
|
5
|
+
Status: VERIFIED
|
|
6
|
+
Approved: Yes
|
|
7
|
+
Iterations: 0
|
|
8
|
+
Worktree: No
|
|
9
|
+
Type: Bugfix
|
|
10
|
+
|
|
11
|
+
## Summary
|
|
12
|
+
|
|
13
|
+
**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`).
|
|
14
|
+
**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.
|
|
15
|
+
**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.
|
|
16
|
+
|
|
17
|
+
**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.
|
|
18
|
+
|
|
19
|
+
## Investigation
|
|
20
|
+
|
|
21
|
+
- **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.
|
|
22
|
+
- **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.
|
|
23
|
+
- **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()`.
|
|
24
|
+
|
|
25
|
+
## Behavior Contract
|
|
26
|
+
|
|
27
|
+
**Given:** UploadInputV3 with `maxFiles=1`, a file is uploaded and the server renames it (response contains a different filename than the original)
|
|
28
|
+
**When:** The parent updates the `value` prop with the server-returned file entry (containing the renamed filename)
|
|
29
|
+
**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`
|
|
30
|
+
**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`).
|
|
31
|
+
**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.
|
|
32
|
+
|
|
33
|
+
## Fix Approach
|
|
34
|
+
|
|
35
|
+
**Chosen:** Remove all completed entries on value change
|
|
36
|
+
**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.
|
|
37
|
+
**Alternatives considered:**
|
|
38
|
+
- *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.
|
|
39
|
+
- *Fuzzy filename matching* — check if value entry's filename contains the base name; fragile, depends on server naming convention.
|
|
40
|
+
|
|
41
|
+
**Files:**
|
|
42
|
+
- `src/components/inputs/upload-input-v3/index.js` (primary fix — useEffect cleanup)
|
|
43
|
+
- `src/components/inputs/dropzone/index.js` (secondary fix — missing return)
|
|
44
|
+
- `src/components/inputs/upload-input-v3/__tests__/upload-input-v3.test.js` (reproducing test)
|
|
45
|
+
|
|
46
|
+
**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.
|
|
47
|
+
|
|
48
|
+
**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).
|
|
49
|
+
|
|
50
|
+
## Verification Scenario
|
|
51
|
+
|
|
52
|
+
### TS-001: Server-Renamed File Upload
|
|
53
|
+
**Preconditions:** UploadInputV3 with maxFiles=1, server configured to rename uploaded files
|
|
54
|
+
|
|
55
|
+
| Step | Action | Expected Result (after fix) |
|
|
56
|
+
|------|--------|-----------------------------|
|
|
57
|
+
| 1 | Upload a single file via UploadInputV3 | File appears as "Loading" then "Complete" |
|
|
58
|
+
| 2 | Parent updates value with server-renamed file | File appears exactly once (from value), no duplicate entry, no "max files" error |
|
|
59
|
+
|
|
60
|
+
## Progress
|
|
61
|
+
|
|
62
|
+
- [x] Task 1: Write Reproducing Test (RED)
|
|
63
|
+
- [x] Task 2: Implement Fix at Root Cause
|
|
64
|
+
- [x] Task 3: Quality Gate
|
|
65
|
+
**Tasks:** 3 | **Done:** 3
|
|
66
|
+
|
|
67
|
+
## Tasks
|
|
68
|
+
|
|
69
|
+
### Task 1: Write Reproducing Test (RED)
|
|
70
|
+
|
|
71
|
+
**Objective:** Encode the Behavior Contract as a failing test BEFORE writing any fix code.
|
|
72
|
+
**Files:** `src/components/inputs/upload-input-v3/__tests__/upload-input-v3.test.js`
|
|
73
|
+
**Entry point:** `UploadInputV3` component (rendered with mock DropzoneV3)
|
|
74
|
+
**Test scenario:**
|
|
75
|
+
1. Render UploadInputV3 with `maxFiles=1` and `value=[]`
|
|
76
|
+
2. Simulate `onAddedFile({ name: 'image.png', size: 75000 })`
|
|
77
|
+
3. Simulate `onFileCompleted({ name: 'image.png', size: 75000 })`
|
|
78
|
+
4. Re-render with `value=[{ filename: 'image_abc123.png', size: 75000 }]` (server-renamed)
|
|
79
|
+
5. Assert: only ONE file entry with text `image_abc123.png` is visible; original `image.png` is NOT visible
|
|
80
|
+
**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`.
|
|
81
|
+
**Verify:** `npx jest src/components/inputs/upload-input-v3/__tests__/upload-input-v3.test.js --verbose`
|
|
82
|
+
|
|
83
|
+
### Task 2: Implement Fix at Root Cause
|
|
84
|
+
|
|
85
|
+
**Objective:** Minimal change to fix the useEffect cleanup and the missing return.
|
|
86
|
+
**Files:**
|
|
87
|
+
- `src/components/inputs/upload-input-v3/index.js` — change useEffect at line 160-166 to remove all completed entries when value has entries
|
|
88
|
+
- `src/components/inputs/dropzone/index.js` — add `return` after `done('Max files reached.')` at line 156
|
|
89
|
+
**Strategy:**
|
|
90
|
+
1. In `upload-input-v3/index.js`, change the useEffect cleanup from:
|
|
91
|
+
```javascript
|
|
92
|
+
setUploadingFiles(prev => prev.filter(f => {
|
|
93
|
+
if (!f.complete) return true;
|
|
94
|
+
return !value.some(v => v.filename === f.name);
|
|
95
|
+
}));
|
|
96
|
+
```
|
|
97
|
+
To:
|
|
98
|
+
```javascript
|
|
99
|
+
setUploadingFiles(prev => prev.filter(f => !f.complete));
|
|
100
|
+
```
|
|
101
|
+
2. In `dropzone/index.js`, add `return;` after `done('Max files reached.');` at line 156.
|
|
102
|
+
**DoD:** Reproducing test PASSES. Full test suite PASSES. Diff touches root-cause files.
|
|
103
|
+
**Verify:** `npx jest --verbose`
|
|
104
|
+
|
|
105
|
+
### Task 3: Quality Gate
|
|
106
|
+
|
|
107
|
+
**Objective:** Lint + build clean, full suite re-run.
|
|
108
|
+
**DoD:** Full suite green, no lint errors, build succeeds.
|
|
109
|
+
**Verify:** `npx jest --verbose && npm run build-dev`
|