Repository navigation
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #1671 +/- ##
============================================
+ Coverage 61.96% 62.17% +0.21%
- Complexity 11285 11311 +26
============================================
Files 1250 1253 +3
Lines 48437 48455 +18
Branches 6796 6796
============================================
+ Hits 30014 30128 +114
+ Misses 15729 15632 -97
- Partials 2694 2695 +1 🚀 New features to boost your workflow:
|
1af6079 to
bd2441e
Compare
GitHub issue #1670 FileUploadToResourceField and the resource it uploads to had several problems: - A failed upload ended on the error page instead of in onUploadFailure(). For a failure other than a limit the resource answers the key uploadFailed, whose text expects an exception in ${exception.localizedMessage}; the field put the key, a String, in its place, and the interpolation threw. Such failures now have a text of their own, uploadToResourceFailed ("The upload failed."). - The field took whatever the browser reported as a resource key. The status text of an HTTP error, or the exception message the resource sent with debug logging on, threw MissingResourceException, and an empty one (HTTP/2, a network failure) read "File must be less than ...". The field now accepts the keys of the limits, of no files selected and of a lost connection, and reads anything else as a failed upload. The script reports HTTP errors and answers that are not JSON as uploadFailed, and a request that got no answer as uploadConnectionLost, which the field words "The upload failed: the connection to the server was lost.". The check is the protected getUploadErrorMessage(String), so a subclass whose own script reports other keys can word them; the field still never looks up a key it does not know, since the browser can send any. The resource no longer sends exception messages to the browser; it logs the exception as an error, which it did not do at all before. - The script checks the limits of the field before sending: files beyond them are reported with the key of the limit and not sent. Before, the server refused them, often by resetting the connection, which the browser saw as a network failure. - A second upload started while one ran sent a second request, which shared the progress information of the first in the session. Whichever ended first removed it, and the other failed with an IllegalStateException. The script now refuses to start an upload while one of the field runs. - The script reported the received files to the field by GET, with their JSON in the URL. Beyond some 65 files that exceeded the 8 KB request line of Jetty and Tomcat: the server answered 414 and onUploadSuccess() was never called, although the files were stored. They are reported by POST now. - FolderUploadsFileManager stored each file under its client file name, so two files of one name, in one upload or in two, overwrote each other while the field was told of both. Files are now stored under a random name, which the resource sends to the browser with the client file name and UploadInfo carries back to the field. - cancel() after an upload had ended still claimed to cancel it, the return value of the before-send callback was ignored, and an answer that was not JSON left the field waiting. Now cancel() does nothing once the upload has ended, false from the before-send callback keeps the upload from being sent and calls onUploadCanceled(), and an answer that is not JSON fails the upload. The Javadoc of AbstractFileUploadResource said the field uploads a single file with jQuery; it uploads all the files of its input with a plain XMLHttpRequest. Keeping files of one name apart needs a name for each stored file that travels from the file manager through the browser back to the field, so the API changes: - IUploadsFileManager gets store(FileUpload, String), which returns the name the file was stored under, and getFile(String, String) takes that name. save() is deprecated, and Wicket no longer calls it. The default store() calls save() and returns the client file name, so an implementation written for save() keeps working, with the old behaviour for files of one name. The default save() throws an UnsupportedOperationException saying to implement store(): save() used to be abstract, and with two defaults calling each other an implementation overriding neither would compile and then fail with a StackOverflowError. - AbstractFileUploadResource.saveFiles() returns the stored names, and prepareResponse() and generateJsonResponse() receive them. These are hooks for subclasses: an old signature kept as deprecated would no longer be called, so the signatures change and an override fails to compile rather than being skipped silently. - FileUploadResourceReference.generateFileInfo() receives the stored name and puts it into the JSON as fileName. - UploadInfo carries the stored name (getFileName() and a new constructor), and equals() and hashCode() use it instead of the client file name. The old constructor is deprecated. A JSON answer without fileName, from a custom resource, falls back to the client file name. - FileUploadToResourceField gets the constants UPLOAD_FAILED_RESOURCE_KEY and CONNECTION_LOST_RESOURCE_KEY, and getUploadErrorMessage(String) becomes protected. Both are additions. What an application sees after the change: files stored by FolderUploadsFileManager no longer have their client file name on disk, so code looking them up with getFile(fieldId, clientFileName) has to use UploadInfo#getFile() or #getFileName() instead. A failed upload calls onUploadFailure() with "The upload failed."; an application that had changed the text of uploadFailed to work around the error page changes uploadToResourceFailed instead. An upload whose connection breaks off reads "The upload failed: the connection to the server was lost.", and so does one the server refuses by resetting the connection, for a limit the field does not check before sending. The client-side error callback receives only resource keys as errorMessage, uploadConnectionLost among them. A before-send callback returning false now keeps the upload from being sent. Code calling the deprecated save() on a manager that implements only store() gets an UnsupportedOperationException, and calls store() instead. migration-to-11.md describes the migration in detail: for each change, what it was before, what it is now, who has to change something and how. The comment of the MigrateToWicket11 recipe has the short version, as for the other changes of Wicket 11 that need manual migration. Draft for the migration guide on the wiki: Uploads to a resource (FileUploadToResourceField): implement IUploadsFileManager#store(FileUpload, String) instead of the deprecated save(). It stores the file under a name of its own and returns that name, which getFile(uploadFieldId, fileName) then receives; the name goes to the browser and back, so getFile() must not return a file of another upload field. FolderUploadsFileManager stores files under random names: find them with UploadInfo#getFile() or UploadInfo#getFileName(), not by client file name. Subclasses of AbstractFileUploadResource receive the stored names in saveFiles(), prepareResponse() and generateJsonResponse(), subclasses of FileUploadResourceReference in generateFileInfo(). A failed upload reads "The upload failed." (resource key uploadToResourceFailed) instead of ending on the error page.
1824d00 to
208e963
Compare
GitHub issue #1669 FileUploadToResourceField uploaded the files of its input as the browser's dialog left them: each choice replaced the one before, files could not be dropped, and a file could not be taken out again before the upload. Two behaviors add that: - FileDropZoneBehavior makes a component a zone files are dropped on. The input receives the dropped files it accepts by its accept attribute, all of them if it is multiple, else the first; folders are left out. It then fires its input and change events, as for files chosen in the dialog. While files are dragged over the zone, it has the CSS class wicket-upload-dragover. - FileListBehavior, on a <ul> or <ol>, lists the files chosen for the next upload, each with a button removing it, whose text is the resource uploadToResourceRemove. With a list the field collects files: if its input is multiple, files chosen or dropped are added to the listed ones instead of replacing them. The files an upload sent leave the list once it succeeded, while files chosen meanwhile stay; during an upload the remove buttons are disabled. A file name is written as text, never as markup. The script keeps the chosen files and writes them back into the input through a DataTransfer, so the upload, the progress bar and other listeners of the input see all of them. It hears of a choice in the capture phase, before the input's other change listeners run. A field without these behaviors works as before: a choice replaces the files of the input, and the input keeps them after the upload. The script's guard against being evaluated twice tested for an object, but Wicket.FileUploadToResourceField is a function, so a second evaluation replaced it. It tests for a function now, as the registry of file lists lives in the script. A test runs the script a second time and checks that the field and its lists survive it. The upload-to-resource example shows a drop zone and a file list. Its styles never applied: they were in a <head> the page ignores, since it extends WicketExamplePage, and inline, which the Content Security Policy of the examples blocks. They are in UploadToResourcePage.css now, under a class name that does not clash with the drop-zone of the examples' style.css. Its guard against leaving the page during an upload used jQuery, which the plain JavaScript engine does not load; it uses Wicket.Event now. Its explanation also says that the limits of the field are checked before anything is sent, and that files of the same name are kept apart, each stored under a name of its own. migration-to-11.md lists the two behaviors among the changes of Wicket 11 that need no migration.
208e963 to
e57325b
Compare
| 'use strict'; | ||
|
|
||
| if (typeof(Wicket.FileUploadToResourceField) === 'object') { | ||
| if (typeof(Wicket.FileUploadToResourceField) === 'function') { |
There was a problem hiding this comment.
Note for reviewers: this guard keeps the script from setting itself up a second time when it reaches the page twice, for example once on its own and once in an aggregated bundle.
On master it compared with 'object', which never matches: Wicket.FileUploadToResourceField is a constructor, so typeof gives 'function'. A second run therefore replaced the constructor and all its methods. That did no visible harm while the script kept no state of its own.
This PR gives the script state: lists, the file lists of each field, which FileListBehavior registers through addList(). With the old guard, a second run would start a new, empty lists. Lists registered after it would be invisible to fields created before it, and those fields would stop collecting files. With 'function', the second run returns at once and everything shares one registry.
The 'object' check looks copied from scripts whose namespace is a plain object, such as Wicket.FilesSelected and Wicket.Head, where it is right.
The QUnit test "a second run of the script keeps the field and its lists" in upload-to-resource.js loads the script again. With 'object', all three of its assertions fail.
Closes #1670
Closes #1669
Two commits, one per issue; the second builds on the first. The follow-ups from review are folded into the commit they belong to.
Make uploads to a resource fail cleanly and keep files of one name apart (#1670)
onUploadFailure()with "The upload failed." (new keyuploadToResourceFailed) instead of ending on the error page.MissingResourceExceptionor claiming the file is too large.uploadConnectionLost, worded "The upload failed: the connection to the server was lost." The key check is the protectedgetUploadErrorMessage(String), so a subclass can word keys its own script reports.onUploadSuccess()was never called.FolderUploadsFileManagerstores files under random names, so two files with the same name no longer overwrite each other.cancel()does nothing once the upload has ended, and a before-send callback returningfalsekeeps the upload from being sent.Add a drop zone and a file list to uploads to a resource (#1669)
FileDropZoneBehaviormakes any component a drop target for the files of a field. It respects theacceptandmultipleattributes of the input, leaves folders out, and setswicket-upload-dragoverwhile files are dragged over it.FileListBehavior, on a<ul>or<ol>, lists the files chosen for the next upload, each with a Remove button (keyuploadToResourceRemove). With a list and amultipleinput, files chosen or dropped add up instead of replacing each other. Uploaded files leave the list, failed ones stay.<head>the page ignores and inline, which the CSP of the examples blocks, so they are inUploadToResourcePage.cssnow. Its guard against leaving the page during an upload used jQuery and failed under the plain JavaScript engine. Its explanation also says that the limits are checked before anything is sent and that files of one name are kept apart.API changes and migration
Keeping files of one name apart needs a stored name that travels from the file manager through the browser back to the field, so on master:
IUploadsFileManagergetsstore(FileUpload, String), which returns the stored name.save()is deprecated for removal, and Wicket no longer calls it. The defaultstore()callssave(), so an implementation written forsave()keeps working, with the old behaviour for files of one name. The defaultsave()throws anUnsupportedOperationException, so a manager that implements neither method fails with a clear message at its first upload rather than with aStackOverflowError.AbstractFileUploadResource.saveFiles()returns the stored names, andprepareResponse()andgenerateJsonResponse()receive them.FileUploadResourceReference.generateFileInfo()receives the stored name as well. These are hooks for subclasses, so the signatures change rather than keeping deprecated ones that would no longer be called: an override fails to compile instead of being skipped silently.UploadInfocarries the stored name (getFileName(), new constructor, the old one is deprecated).equals()andhashCode()use it.FileUploadToResourceField.getUploadErrorMessage(String)is protected, and the constantsUPLOAD_FAILED_RESOURCE_KEYandCONNECTION_LOST_RESOURCE_KEYname the keys of a failed upload and of a lost connection.The full before and after of every change, who has to act and how, is in
migration-to-11.md.There is no OpenRewrite recipe, because none of this is mechanical. Like the other Wicket 11 changes that need manual migration, the note is in the comment of the
MigrateToWicket11recipe inwicket.yml. There is no "Migration to Wicket 11.0" page on the wiki yet. Draft wording for it:No backport: wicket-10.x stays as it is.
Tests
FileUploadToResourceFieldTest,FileListBehaviorTest,FileDropZoneBehaviorTest,IUploadsFileManagerTestfor the defaults ofstore()andsave(), andstore()tests inFolderUploadsFileManagerTest.upload-to-resource.js, run against both the jQuery and the plain JavaScript engine.mvn clean install -Pjs-testis green on each of the two commits.