Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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": {
Expand Down
1 change: 1 addition & 0 deletions src/components/inputs/dropzone/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -308,5 +308,119 @@ describe('UploadInputV3', () => {
const { container } = render(<UploadInputV3 postUrl="https://example.com/upload" id="test" mediaType={defaultProps.mediaType} />);
expect(container.firstChild).not.toBeNull();
});

test('cleans up completed uploading file when value updates with server-renamed filename', () => {
const { rerender } = render(<UploadInputV3 {...defaultProps} value={[]} maxFiles={1} />);

// 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(<UploadInputV3 {...defaultProps} value={[{ filename: 'image_abc123.png', size: 75000 }]} maxFiles={1} />);

// 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(<UploadInputV3 {...defaultProps} value={[]} maxFiles={1} />);

// 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(<UploadInputV3 {...defaultProps} value={[]} maxFiles={1} />);

// 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(<UploadInputV3 {...defaultProps} value={[]} maxFiles={3} />);

// 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();
});
});
});
13 changes: 8 additions & 5 deletions src/components/inputs/upload-input-v3/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down Expand Up @@ -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]);
Comment thread
coderabbitai[bot] marked this conversation as resolved.

Expand Down
Loading