diff --git a/.changeset/cancel-during-transform.md b/.changeset/cancel-during-transform.md new file mode 100644 index 000000000..0b8a896b2 --- /dev/null +++ b/.changeset/cancel-during-transform.md @@ -0,0 +1,5 @@ +--- +"dropzone": patch +--- + +Fix cancelling an upload while `transformFile` is still running. The file had no `xhr` yet, so it was grouped with every other file that had not started and the whole queue was cancelled with it — and the file itself was still sent once the transform finished. diff --git a/packages/dropzone/src/dropzone.ts b/packages/dropzone/src/dropzone.ts index 89131f7ec..c833c14d4 100644 --- a/packages/dropzone/src/dropzone.ts +++ b/packages/dropzone/src/dropzone.ts @@ -1345,7 +1345,12 @@ export default class Dropzone extends Emitter { // set to CANCELED. cancelUpload(file: DropzoneFile) { if (file.status === Dropzone.UPLOADING) { - let groupedFiles = this._getFilesWithXhr(file.xhr!); + // A file cancelled while `transformFile` is still running is already + // UPLOADING but has no request yet. Grouping by an undefined `xhr` then + // matched every other file that had not started either -- the whole + // queue -- and cancelled all of them. Without a request there is nobody + // to share it with, so the group is just this file. See #2231. + let groupedFiles = file.xhr ? this._getFilesWithXhr(file.xhr) : [file]; for (let groupedFile of groupedFiles) { groupedFile.status = Dropzone.CANCELED; } @@ -1395,6 +1400,14 @@ export default class Dropzone extends Emitter { uploadFiles(files: DropzoneFile[]) { this._transformFiles(files, (transformedFiles: (DropzoneFile | Blob)[]) => { + // `transformFile` is asynchronous and can take a while -- resizing or + // compressing a large image, or asking a server to presign the upload -- + // and the user can cancel or remove the file in the meantime. Sending it + // now would upload data the UI already reported as canceled. See #2231. + if (files.every((file) => file.status === Dropzone.CANCELED)) { + return; + } + // Options are often read straight out of markup or a config file, so // `chunkSize` can arrive as a string. `start + "2097152"` would then // concatenate instead of adding, and every chunk after the first would diff --git a/packages/dropzone/test/unit-tests/all.js b/packages/dropzone/test/unit-tests/all.js index 7d00c6452..4e4f4afea 100644 --- a/packages/dropzone/test/unit-tests/all.js +++ b/packages/dropzone/test/unit-tests/all.js @@ -692,6 +692,65 @@ describe("Dropzone", function () { }, 10); })); + it("should not cancel other queued files if the upload has not started yet", () => + new Promise((done) => { + dropzone.accept = (file, done) => done(); + // A transform that never finishes: the file is UPLOADING but has no + // xhr, which is what used to group it with every queued file. + dropzone.options.transformFile = () => {}; + dropzone.options.parallelUploads = 1; + + let uploading = getMockFile("text/html", "uploading"); + let queued1 = getMockFile("text/html", "queued1"); + let queued2 = getMockFile("text/html", "queued2"); + + dropzone.addFile(uploading); + dropzone.addFile(queued1); + dropzone.addFile(queued2); + + return setTimeout(function () { + expect(uploading.status).toBe(Dropzone.UPLOADING); + expect(uploading.xhr).toBe(undefined); + + let canceled = []; + dropzone.on("canceled", (file) => canceled.push(file.upload.filename)); + + dropzone.cancelUpload(uploading); + + expect(uploading.status).toBe(Dropzone.CANCELED); + // Only the file the user cancelled. The queue then moves on, so + // the next one starts and the last one waits its turn. + expect(canceled).toEqual(["uploading"]); + expect([queued1.status, queued2.status]).toEqual([Dropzone.UPLOADING, Dropzone.QUEUED]); + return done(); + }, 10); + })); + + it("should not send a file that was canceled while it was being transformed", () => + new Promise((done) => { + dropzone.accept = (file, done) => done(); + + let finishTransform = null; + dropzone.options.transformFile = (file, transformDone) => + (finishTransform = () => transformDone(file)); + + let mockFile = getMockFile(); + dropzone.addFile(mockFile); + + return setTimeout(function () { + expect(mockFile.status).toBe(Dropzone.UPLOADING); + + dropzone.cancelUpload(mockFile); + expect(mockFile.status).toBe(Dropzone.CANCELED); + + // The transform only finishes now, after the user gave up on it. + finishTransform(); + + expect(mockFile.xhr).toBe(undefined); + return done(); + }, 10); + })); + it("should properly cancel all files with the same XHR if uploadMultiple is true", () => new Promise((done) => { let mock1 = getMockFile();