diff --git a/package.json b/package.json
index b13b90cd..05558619 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "openstack-uicore-foundation",
- "version": "5.0.16-beta.1",
+ "version": "5.0.15-beta.2",
"description": "ui reactjs components for openstack marketing site",
"main": "lib/openstack-uicore-foundation.js",
"scripts": {
diff --git a/src/components/inputs/dropzone/index.js b/src/components/inputs/dropzone/index.js
index 1706d255..514510b5 100644
--- a/src/components/inputs/dropzone/index.js
+++ b/src/components/inputs/dropzone/index.js
@@ -154,6 +154,7 @@ export class DropzoneJS extends React.Component {
}
if (options.maxFiles && options.maxFiles < (this.state.files.length + this.props.uploadCount)) {
done('Max files reached.');
+ return;
}
done();
diff --git a/src/components/inputs/upload-input-v3/__tests__/upload-input-v3.test.js b/src/components/inputs/upload-input-v3/__tests__/upload-input-v3.test.js
index ebf37d8d..78e26f44 100644
--- a/src/components/inputs/upload-input-v3/__tests__/upload-input-v3.test.js
+++ b/src/components/inputs/upload-input-v3/__tests__/upload-input-v3.test.js
@@ -308,5 +308,119 @@ describe('UploadInputV3', () => {
const { container } = render();
expect(container.firstChild).not.toBeNull();
});
+
+ test('cleans up completed uploading file when value updates with server-renamed filename', () => {
+ const { rerender } = render();
+
+ // Simulate file added
+ act(() => {
+ dropzoneCallbacks.onAddedFile({ name: 'image.png', size: 75000 });
+ });
+
+ // Simulate file completed
+ act(() => {
+ dropzoneCallbacks.onFileCompleted({ name: 'image.png', size: 75000 });
+ });
+
+ // Parent updates value with server-renamed file
+ rerender();
+
+ // Assert: only the server-renamed file is visible
+ expect(screen.getByText('image_abc123.png')).toBeInTheDocument();
+ expect(screen.queryByText('image.png')).not.toBeInTheDocument();
+ });
+
+ test('does not mark file as complete when _asyncProcessing is true', () => {
+ render();
+
+ // Simulate file added
+ act(() => {
+ dropzoneCallbacks.onAddedFile({ name: 'video.mp4', size: 5000000 });
+ });
+
+ // Simulate file completed with _asyncProcessing flag (HTTP 202 case)
+ act(() => {
+ dropzoneCallbacks.onFileCompleted({ name: 'video.mp4', size: 5000000, _asyncProcessing: true });
+ });
+
+ // Assert: file should still show "Loading" (not "Complete") because async processing is in progress
+ expect(screen.getByText('video.mp4')).toBeInTheDocument();
+ expect(screen.getByText(/Loading/)).toBeInTheDocument();
+ expect(screen.queryByText(/Complete/)).not.toBeInTheDocument();
+ });
+
+ test('async file transitions from Loading to Complete when onUploadComplete fires', () => {
+ render();
+
+ // Simulate file added
+ act(() => {
+ dropzoneCallbacks.onAddedFile({ name: 'video.mp4', size: 5000000 });
+ });
+
+ // All chunks finish uploading — progress reaches 100 before async polling starts
+ act(() => {
+ dropzoneCallbacks.onUploadProgress({ name: 'video.mp4', size: 5000000 }, 100);
+ });
+
+ // handleFileCompleted fires with _asyncProcessing flag (HTTP 202 case) — file stays Loading
+ act(() => {
+ dropzoneCallbacks.onFileCompleted({ name: 'video.mp4', size: 5000000, _asyncProcessing: true });
+ });
+
+ // File should still be Loading despite progress being 100
+ expect(screen.getByText(/Loading/)).toBeInTheDocument();
+ expect(screen.queryByText(/Complete/)).not.toBeInTheDocument();
+
+ // Polling completes — onUploadComplete fires after async processing finishes
+ act(() => {
+ dropzoneCallbacks.onUploadComplete({ name: 'video_final.mp4' }, 'test-upload', {});
+ });
+
+ // Assert: file should now show "Complete"
+ expect(screen.getByText('video.mp4')).toBeInTheDocument();
+ expect(screen.getByText(/Complete/)).toBeInTheDocument();
+ expect(screen.queryByText(/Loading/)).not.toBeInTheDocument();
+ });
+
+ test('completing one file does not mark other in-flight files as complete when maxFiles > 1', () => {
+ render();
+
+ // Simulate two files added
+ act(() => {
+ dropzoneCallbacks.onAddedFile({ name: 'file-a.png', size: 10000 });
+ });
+ act(() => {
+ dropzoneCallbacks.onAddedFile({ name: 'file-b.png', size: 20000 });
+ });
+
+ // Both should be Loading
+ expect(screen.getByText('file-a.png')).toBeInTheDocument();
+ expect(screen.getByText('file-b.png')).toBeInTheDocument();
+ expect(screen.getAllByText(/Loading/)).toHaveLength(2);
+
+ // File A finishes uploading all chunks — progress reaches 100
+ act(() => {
+ dropzoneCallbacks.onUploadProgress({ name: 'file-a.png', size: 10000 }, 100);
+ });
+
+ // File B is still mid-upload — progress at 40
+ act(() => {
+ dropzoneCallbacks.onUploadProgress({ name: 'file-b.png', size: 20000 }, 40);
+ });
+
+ // File A completes (sync path): handleFileCompleted marks A complete
+ act(() => {
+ dropzoneCallbacks.onFileCompleted({ name: 'file-a.png', size: 10000 });
+ });
+
+ // Then onUploadComplete fires for file A
+ act(() => {
+ dropzoneCallbacks.onUploadComplete({ name: 'file-a.png' }, 'test-upload', {});
+ });
+
+ // Assert: file B should still show "Loading" — it has not finished uploading
+ expect(screen.getByText('file-b.png')).toBeInTheDocument();
+ expect(screen.getByText(/Loading/)).toBeInTheDocument();
+ });
});
});
diff --git a/src/components/inputs/upload-input-v3/index.js b/src/components/inputs/upload-input-v3/index.js
index a1b31b1d..d90e6a75 100644
--- a/src/components/inputs/upload-input-v3/index.js
+++ b/src/components/inputs/upload-input-v3/index.js
@@ -151,18 +151,18 @@ const UploadInputV3 = ({
// Mark as complete instead of removing — keep it visible until value is updated by the parent
const handleFileCompleted = useCallback((file) => {
+ // Skip marking complete for async processing (HTTP 202) — file stays "Loading" until polling confirms
+ if (file._asyncProcessing) return;
+
setUploadingFiles(prev => prev.map(f =>
f.name === file.name && f.size === file.size ? { ...f, progress: 100, complete: true } : f
));
}, []);
- // Once the parent updates value, remove the matching completed file from uploadingFiles
+ // Once the parent updates value, remove all completed files from uploadingFiles
useEffect(() => {
if (uploadingFiles.length === 0 || value.length === 0) return;
- setUploadingFiles(prev => prev.filter(f => {
- if (!f.complete) return true;
- return !value.some(v => v.filename === f.name);
- }));
+ setUploadingFiles(prev => prev.filter(f => !f.complete));
}, [value]);
const handleFileError = useCallback((file, message) => {
@@ -191,6 +191,9 @@ const UploadInputV3 = ({
}, []);
const wrappedOnUploadComplete = useCallback((response, dzId, dzData) => {
+ // Mark fully-uploaded rows complete (covers HTTP 202 flow where handleFileCompleted was skipped).
+ // Guard against flipping rows whose bytes are still in flight when maxFiles > 1.
+ setUploadingFiles(prev => prev.map(f => (f.progress >= 100 ? { ...f, complete: true } : f)));
if (onUploadComplete) onUploadComplete(response, dzId, dzData);
}, [onUploadComplete]);