Jelajahi Sumber

Merge branch 'master' into feature/remote-late-add

Artur Paikin 6 tahun lalu
induk
melakukan
6ecf71f0cf
56 mengubah file dengan 1862 tambahan dan 379 penghapusan
  1. 2 2
      CHANGELOG.md
  2. 409 0
      package-lock.json
  3. 5 0
      package.json
  4. 15 8
      src/core/Core.js
  5. 71 70
      src/core/Core.test.js
  6. 3 2
      src/core/Plugin.js
  7. 10 9
      src/plugins/AwsS3/Multipart.js
  8. 4 2
      src/plugins/Dashboard/Dashboard.js
  9. 2 2
      src/plugins/Dashboard/FileCard.js
  10. 52 26
      src/plugins/Dashboard/FileItem.js
  11. 13 10
      src/plugins/Dashboard/FileItemProgress.js
  12. 2 1
      src/plugins/Dashboard/FileList.js
  13. 1 1
      src/plugins/Dashboard/Tabs.js
  14. 81 50
      src/plugins/Dashboard/index.js
  15. 20 12
      src/plugins/DragDrop/index.js
  16. 5 1
      src/plugins/Dummy.js
  17. 10 6
      src/plugins/FileInput.js
  18. 8 9
      src/plugins/GoldenRetriever/index.js
  19. 4 4
      src/plugins/ReduxDevTools.js
  20. 9 8
      src/plugins/StatusBar/StatusBar.js
  21. 2 2
      src/plugins/StatusBar/index.js
  22. 2 7
      src/plugins/ThumbnailGenerator/index.js
  23. 4 4
      src/plugins/ThumbnailGenerator/index.test.js
  24. 11 16
      src/plugins/Transloadit/index.js
  25. 11 18
      src/plugins/Tus.js
  26. 5 1
      src/plugins/Url/index.js
  27. 3 2
      src/plugins/Webcam/RecordButton.js
  28. 12 2
      src/plugins/Webcam/index.js
  29. 34 8
      src/plugins/XHRUpload.js
  30. 1 1
      src/react/Dashboard.js
  31. 35 0
      src/react/Dashboard.test.js
  32. 1 1
      src/react/DashboardModal.js
  33. 81 0
      src/react/DashboardModal.test.js
  34. 1 1
      src/react/DragDrop.js
  35. 35 0
      src/react/DragDrop.test.js
  36. 1 1
      src/react/ProgressBar.js
  37. 35 0
      src/react/ProgressBar.test.js
  38. 1 1
      src/react/StatusBar.js
  39. 35 0
      src/react/StatusBar.test.js
  40. 18 0
      src/react/__mocks__/DashboardPlugin.js
  41. 18 0
      src/react/__mocks__/DragDropPlugin.js
  42. 18 0
      src/react/__mocks__/ProgressBarPlugin.js
  43. 18 0
      src/react/__mocks__/StatusBarPlugin.js
  44. 0 23
      src/scss/_animation.scss
  45. 61 61
      src/scss/_dashboard.scss
  46. 5 1
      src/scss/_statusbar.scss
  47. 4 3
      src/server/RequestClient.js
  48. 3 1
      src/views/ProviderView/AuthView.js
  49. 5 1
      src/views/ProviderView/index.js
  50. 581 0
      types/index.d.ts
  51. 28 0
      types/tsconfig.json
  52. 1 0
      types/tslint.json
  53. 7 0
      types/uppy-tests-official.ts
  54. 36 0
      types/uppy-tests.ts
  55. 13 1
      website/src/docs/dashboard.md
  56. 10 0
      website/src/docs/uppy.md

+ 2 - 2
CHANGELOG.md

@@ -41,7 +41,7 @@ PRs are welcome! Please do open an issue to discuss first if it's a big feature,
 - [ ] statusbar: add option to always show
 - [ ] have a `resetProgress` method for resetting a single file, and call it before starting an upload. see comment in #393
 - [ ] “Custom Provider” plugin for  Dashboard — shows already uploaded files or files from a custom service; accepts an array of files to show in options, no uppy-server required #362
-- [ ] WordPress plugin
+- [ ] WordPress plugin https://www.producthunt.com/posts/uppy-io#comment-559327 (“And Gravity forms”)
 - [ ] Transformations, cropping, filters for images, see #53
 - [ ] Prepare for (piwik-) tracking of usage of uppy ? see #83
 - [ ] screenshot+screencast support similar to Webcam #148
@@ -125,7 +125,7 @@ To Be Released: 2018-05-31.
 - [x] providers: select files only after “select” is pressed, don’t add them right away when they are checked (keep a list of fileIds in state?); better UI + solves issue with autoProceed uploading in background, which is weird; re-read https://github.com/transloadit/uppy/pull/419#issuecomment-345210519 (@arturi, @goto-bus-stop)
 - [x] tus: add `filename` and `filetype`, so that tus servers knows what headers to set  https://github.com/tus/tus-js-client/commit/ebc5189eac35956c9f975ead26de90c896dbe360 (#844 / @vith)
 - [ ] core: look into utilizing https://github.com/que-etc/resize-observer-polyfill for responsive components. See also https://github.com/transloadit/uppy/issues/750
-- [x] core: ⚠️ **breaking** removed .run() (to solve issues like #756), update ddocs (#793 / goto-bus-stop)
+- [x] core: ⚠️ **breaking** removed .run() (to solve issues like #756), update docs (#793 / goto-bus-stop)
 - [ ] core: use Browserslist config to share between PostCSS, Autoprefixer and Babel https://github.com/browserslist/browserslist, https://github.com/amilajack/eslint-plugin-compat (@arturi)
 - [ ] core: utilize https://github.com/jonathantneal/postcss-preset-env, maybe https://github.com/jonathantneal/postcss-normalize (@arturi)
 - [ ] core: addFile not passing restrictions shouldn’t throw when called from UI

+ 409 - 0
package-lock.json

@@ -101,6 +101,12 @@
       "integrity": "sha1-yRNQTT3CgQr61VW1ma6uwsxMZ2g=",
       "dev": true
     },
+    "@types/node": {
+      "version": "10.1.3",
+      "resolved": "https://registry.npmjs.org/@types/node/-/node-10.1.3.tgz",
+      "integrity": "sha512-GiCx7dRvta0hbxXoJFAUxz+CKX6bZSCKjM5slq2vPp/5zwK01T4ibYZkGr6EN4F2QmxDQR76/ZHg6q+7iFWCWw==",
+      "dev": true
+    },
     "JSONStream": {
       "version": "1.3.1",
       "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.1.tgz",
@@ -2182,6 +2188,12 @@
         }
       }
     },
+    "boolbase": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
+      "integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24=",
+      "dev": true
+    },
     "boom": {
       "version": "2.10.1",
       "resolved": "https://registry.npmjs.org/boom/-/boom-2.10.1.tgz",
@@ -3249,6 +3261,20 @@
       "integrity": "sha1-7tY7usnqSaDiagljFAWLA7CN1is=",
       "dev": true
     },
+    "cheerio": {
+      "version": "1.0.0-rc.2",
+      "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.2.tgz",
+      "integrity": "sha1-S59TqBsn5NXawxwP/Qz6A8xoMNs=",
+      "dev": true,
+      "requires": {
+        "css-select": "~1.2.0",
+        "dom-serializer": "~0.1.0",
+        "entities": "~1.1.1",
+        "htmlparser2": "^3.9.1",
+        "lodash": "^4.15.0",
+        "parse5": "^3.0.1"
+      }
+    },
     "chokidar": {
       "version": "1.7.0",
       "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-1.7.0.tgz",
@@ -3953,12 +3979,30 @@
         "css": "^2.0.0"
       }
     },
+    "css-select": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/css-select/-/css-select-1.2.0.tgz",
+      "integrity": "sha1-KzoRBTnFNV8c2NMUYj6HCxIeyFg=",
+      "dev": true,
+      "requires": {
+        "boolbase": "~1.0.0",
+        "css-what": "2.1",
+        "domutils": "1.5.1",
+        "nth-check": "~1.0.1"
+      }
+    },
     "css-value": {
       "version": "0.0.1",
       "resolved": "https://registry.npmjs.org/css-value/-/css-value-0.0.1.tgz",
       "integrity": "sha1-Xv1sLupeof1rasV+wEJ7GEUkJOo=",
       "dev": true
     },
+    "css-what": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/css-what/-/css-what-2.1.0.tgz",
+      "integrity": "sha1-lGfQMsOM+u+58teVASUwYvh/ob0=",
+      "dev": true
+    },
     "cssnano": {
       "version": "3.10.0",
       "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-3.10.0.tgz",
@@ -4454,6 +4498,12 @@
         }
       }
     },
+    "discontinuous-range": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/discontinuous-range/-/discontinuous-range-1.0.0.tgz",
+      "integrity": "sha1-44Mx8IRLukm5qctxx3FYWqsbxlo=",
+      "dev": true
+    },
     "dns-prefetch-control": {
       "version": "0.1.0",
       "resolved": "https://registry.npmjs.org/dns-prefetch-control/-/dns-prefetch-control-0.1.0.tgz",
@@ -4478,12 +4528,36 @@
         }
       }
     },
+    "dom-serializer": {
+      "version": "0.1.0",
+      "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.1.0.tgz",
+      "integrity": "sha1-BzxpdUbOB4DOI75KKOKT5AvDDII=",
+      "dev": true,
+      "requires": {
+        "domelementtype": "~1.1.1",
+        "entities": "~1.1.1"
+      },
+      "dependencies": {
+        "domelementtype": {
+          "version": "1.1.3",
+          "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.1.3.tgz",
+          "integrity": "sha1-vSh3PiZCiBrsUVRJJCmcXNgiGFs=",
+          "dev": true
+        }
+      }
+    },
     "domain-browser": {
       "version": "1.1.7",
       "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-1.1.7.tgz",
       "integrity": "sha1-hnqksJP6oF8d4IwG9NeyH9+GmLw=",
       "dev": true
     },
+    "domelementtype": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.0.tgz",
+      "integrity": "sha1-sXrtguirWeUt2cGbF1bg/BhyBMI=",
+      "dev": true
+    },
     "domexception": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/domexception/-/domexception-1.0.1.tgz",
@@ -4493,6 +4567,25 @@
         "webidl-conversions": "^4.0.2"
       }
     },
+    "domhandler": {
+      "version": "2.4.2",
+      "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.4.2.tgz",
+      "integrity": "sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA==",
+      "dev": true,
+      "requires": {
+        "domelementtype": "1"
+      }
+    },
+    "domutils": {
+      "version": "1.5.1",
+      "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.5.1.tgz",
+      "integrity": "sha1-3NhIiib1Y9YQeeSMn3t+Mjc2gs8=",
+      "dev": true,
+      "requires": {
+        "dom-serializer": "0",
+        "domelementtype": "1"
+      }
+    },
     "dont-sniff-mimetype": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/dont-sniff-mimetype/-/dont-sniff-mimetype-1.0.0.tgz",
@@ -4742,6 +4835,12 @@
         "has-binary2": "~1.0.2"
       }
     },
+    "entities": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.1.tgz",
+      "integrity": "sha1-blwtClYhtdra7O+AuQ7ftc13cvA=",
+      "dev": true
+    },
     "envify": {
       "version": "4.1.0",
       "resolved": "https://registry.npmjs.org/envify/-/envify-4.1.0.tgz",
@@ -4760,6 +4859,76 @@
         }
       }
     },
+    "enzyme": {
+      "version": "3.3.0",
+      "resolved": "https://registry.npmjs.org/enzyme/-/enzyme-3.3.0.tgz",
+      "integrity": "sha512-l8csyPyLmtxskTz6pX9W8eDOyH1ckEtDttXk/vlFWCjv00SkjTjtoUrogqp4yEvMyneU9dUJoOLnqFoiHb8IHA==",
+      "dev": true,
+      "requires": {
+        "cheerio": "^1.0.0-rc.2",
+        "function.prototype.name": "^1.0.3",
+        "has": "^1.0.1",
+        "is-boolean-object": "^1.0.0",
+        "is-callable": "^1.1.3",
+        "is-number-object": "^1.0.3",
+        "is-string": "^1.0.4",
+        "is-subset": "^0.1.1",
+        "lodash": "^4.17.4",
+        "object-inspect": "^1.5.0",
+        "object-is": "^1.0.1",
+        "object.assign": "^4.1.0",
+        "object.entries": "^1.0.4",
+        "object.values": "^1.0.4",
+        "raf": "^3.4.0",
+        "rst-selector-parser": "^2.2.3"
+      },
+      "dependencies": {
+        "function-bind": {
+          "version": "1.1.1",
+          "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
+          "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
+          "dev": true
+        },
+        "object.assign": {
+          "version": "4.1.0",
+          "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.0.tgz",
+          "integrity": "sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==",
+          "dev": true,
+          "requires": {
+            "define-properties": "^1.1.2",
+            "function-bind": "^1.1.1",
+            "has-symbols": "^1.0.0",
+            "object-keys": "^1.0.11"
+          }
+        }
+      }
+    },
+    "enzyme-adapter-react-16": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/enzyme-adapter-react-16/-/enzyme-adapter-react-16-1.1.1.tgz",
+      "integrity": "sha512-kC8pAtU2Jk3OJ0EG8Y2813dg9Ol0TXi7UNxHzHiWs30Jo/hj7alc//G1YpKUsPP1oKl9X+Lkx+WlGJpPYA+nvw==",
+      "dev": true,
+      "requires": {
+        "enzyme-adapter-utils": "^1.3.0",
+        "lodash": "^4.17.4",
+        "object.assign": "^4.0.4",
+        "object.values": "^1.0.4",
+        "prop-types": "^15.6.0",
+        "react-reconciler": "^0.7.0",
+        "react-test-renderer": "^16.0.0-0"
+      }
+    },
+    "enzyme-adapter-utils": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/enzyme-adapter-utils/-/enzyme-adapter-utils-1.3.0.tgz",
+      "integrity": "sha512-vVXSt6uDv230DIv+ebCG66T1Pm36Kv+m74L1TrF4kaE7e1V7Q/LcxO0QRkajk5cA6R3uu9wJf5h13wOTezTbjA==",
+      "dev": true,
+      "requires": {
+        "lodash": "^4.17.4",
+        "object.assign": "^4.0.4",
+        "prop-types": "^15.6.0"
+      }
+    },
     "error-ex": {
       "version": "1.3.1",
       "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.1.tgz",
@@ -7498,6 +7667,25 @@
       "integrity": "sha1-FhdnFMgBeY5Ojyz391KUZ7tKV3E=",
       "dev": true
     },
+    "function.prototype.name": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.0.tgz",
+      "integrity": "sha512-Bs0VRrTz4ghD8pTmbJQD1mZ8A/mN0ur/jGz+A6FBxPDUPkm1tNfF6bhTYPA7i7aF4lZJVr+OXTNNrnnIl58Wfg==",
+      "dev": true,
+      "requires": {
+        "define-properties": "^1.1.2",
+        "function-bind": "^1.1.1",
+        "is-callable": "^1.1.3"
+      },
+      "dependencies": {
+        "function-bind": {
+          "version": "1.1.1",
+          "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
+          "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
+          "dev": true
+        }
+      }
+    },
     "functional-red-black-tree": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz",
@@ -7917,6 +8105,12 @@
       "resolved": "https://registry.npmjs.org/has-symbol-support-x/-/has-symbol-support-x-1.4.1.tgz",
       "integrity": "sha512-JkaetveU7hFbqnAC1EV1sF4rlojU2D4Usc5CmS69l6NfmPDnpnFUegzFg33eDkkpNCxZ0mQp65HwUDrNFS/8MA=="
     },
+    "has-symbols": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.0.tgz",
+      "integrity": "sha1-uhqPGvKg/DllD1yFA2dwQSIGO0Q=",
+      "dev": true
+    },
     "has-to-string-tag-x": {
       "version": "1.4.1",
       "resolved": "https://registry.npmjs.org/has-to-string-tag-x/-/has-to-string-tag-x-1.4.1.tgz",
@@ -8110,6 +8304,20 @@
       "integrity": "sha1-OgPtwiFLyjtmQko+eVk0lQnLA1E=",
       "dev": true
     },
+    "htmlparser2": {
+      "version": "3.9.2",
+      "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.9.2.tgz",
+      "integrity": "sha1-G9+HrMoPP55T+k/M6w9LTLsAszg=",
+      "dev": true,
+      "requires": {
+        "domelementtype": "^1.3.0",
+        "domhandler": "^2.3.0",
+        "domutils": "^1.5.1",
+        "entities": "^1.1.1",
+        "inherits": "^2.0.1",
+        "readable-stream": "^2.0.2"
+      }
+    },
     "http-errors": {
       "version": "1.5.1",
       "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.5.1.tgz",
@@ -8395,6 +8603,12 @@
         "binary-extensions": "^1.0.0"
       }
     },
+    "is-boolean-object": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.0.0.tgz",
+      "integrity": "sha1-mPiygDBoQhmpXzdc+9iM40Bd/5M=",
+      "dev": true
+    },
     "is-buffer": {
       "version": "1.1.5",
       "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.5.tgz",
@@ -8600,6 +8814,12 @@
         "lodash.isfinite": "^3.3.2"
       }
     },
+    "is-number-object": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.3.tgz",
+      "integrity": "sha1-8mWrian0RQNO9q/xWo8AsA9VF5k=",
+      "dev": true
+    },
     "is-obj": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz",
@@ -8760,6 +8980,12 @@
       "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.4.tgz",
       "integrity": "sha1-zDqbaYV9Yh6WNyWiTK7shzuCbmQ="
     },
+    "is-subset": {
+      "version": "0.1.1",
+      "resolved": "https://registry.npmjs.org/is-subset/-/is-subset-0.1.1.tgz",
+      "integrity": "sha1-ilkRfZMt4d4A8kX83TnOQ/HpOaY=",
+      "dev": true
+    },
     "is-svg": {
       "version": "2.1.0",
       "resolved": "https://registry.npmjs.org/is-svg/-/is-svg-2.1.0.tgz",
@@ -11353,6 +11579,12 @@
       "integrity": "sha1-9HGh2khr5g9quVXRcRVSPdHSVdU=",
       "dev": true
     },
+    "lodash.flattendeep": {
+      "version": "4.4.0",
+      "resolved": "https://registry.npmjs.org/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz",
+      "integrity": "sha1-+wMJF/hqMTTlvJvsDWngAT3f7bI=",
+      "dev": true
+    },
     "lodash.includes": {
       "version": "4.3.0",
       "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
@@ -11994,6 +12226,26 @@
       "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=",
       "dev": true
     },
+    "nearley": {
+      "version": "2.13.0",
+      "resolved": "https://registry.npmjs.org/nearley/-/nearley-2.13.0.tgz",
+      "integrity": "sha512-ioYYogSaZhFlCpRizQgY3UT3G1qFXmHGY/5ozoFE3dMfiCRAeJfh+IPE3/eh9gCZvqLhPCWb4bLt7Bqzo+1mLQ==",
+      "dev": true,
+      "requires": {
+        "nomnom": "~1.6.2",
+        "railroad-diagrams": "^1.0.0",
+        "randexp": "0.4.6",
+        "semver": "^5.4.1"
+      },
+      "dependencies": {
+        "semver": {
+          "version": "5.5.0",
+          "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz",
+          "integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==",
+          "dev": true
+        }
+      }
+    },
     "negotiator": {
       "version": "0.6.1",
       "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz",
@@ -12767,6 +13019,24 @@
       "integrity": "sha1-WuVUHQJGRdMqWPzdyc7s6nrjrC8=",
       "dev": true
     },
+    "nomnom": {
+      "version": "1.6.2",
+      "resolved": "https://registry.npmjs.org/nomnom/-/nomnom-1.6.2.tgz",
+      "integrity": "sha1-hKZqJgF0QI/Ft3oY+IjszET7aXE=",
+      "dev": true,
+      "requires": {
+        "colors": "0.5.x",
+        "underscore": "~1.4.4"
+      },
+      "dependencies": {
+        "colors": {
+          "version": "0.5.1",
+          "resolved": "https://registry.npmjs.org/colors/-/colors-0.5.1.tgz",
+          "integrity": "sha1-fQAj6usVTo7p/Oddy5I9DtFmd3Q=",
+          "dev": true
+        }
+      }
+    },
     "nopt": {
       "version": "3.0.6",
       "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz",
@@ -12971,6 +13241,15 @@
         "set-blocking": "~2.0.0"
       }
     },
+    "nth-check": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-1.0.1.tgz",
+      "integrity": "sha1-mSms32KPwsQQmN6rgqxYDPFJquQ=",
+      "dev": true,
+      "requires": {
+        "boolbase": "~1.0.0"
+      }
+    },
     "num2fraction": {
       "version": "1.2.2",
       "resolved": "https://registry.npmjs.org/num2fraction/-/num2fraction-1.2.2.tgz",
@@ -13016,6 +13295,18 @@
         "to-property-key-x": "^2.0.1"
       }
     },
+    "object-inspect": {
+      "version": "1.6.0",
+      "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.6.0.tgz",
+      "integrity": "sha512-GJzfBZ6DgDAmnuaM3104jR4s1Myxr3Y3zfIyN4z3UdqN69oSRacNK8UhnobDdC+7J2AHCjGwxQubNJfE70SXXQ==",
+      "dev": true
+    },
+    "object-is": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.0.1.tgz",
+      "integrity": "sha1-CqYOyZiaCz7Xlc9NBvYs8a1lObY=",
+      "dev": true
+    },
     "object-keys": {
       "version": "1.0.11",
       "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.0.11.tgz",
@@ -13350,6 +13641,15 @@
         "error-ex": "^1.2.0"
       }
     },
+    "parse5": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/parse5/-/parse5-3.0.3.tgz",
+      "integrity": "sha512-rgO9Zg5LLLkfJF9E6CCmXlSE4UVceloys8JrFqCcHloC3usd/kJCyPDwH2SOlzix2j3xaP9sUX3e8+kvkuleAA==",
+      "dev": true,
+      "requires": {
+        "@types/node": "*"
+      }
+    },
     "parseqs": {
       "version": "0.0.5",
       "resolved": "https://registry.npmjs.org/parseqs/-/parseqs-0.0.5.tgz",
@@ -14477,6 +14777,39 @@
       "integrity": "sha1-EIOSF/bBNiuJGUBE0psjP9fzLwE=",
       "dev": true
     },
+    "raf": {
+      "version": "3.4.0",
+      "resolved": "https://registry.npmjs.org/raf/-/raf-3.4.0.tgz",
+      "integrity": "sha512-pDP/NMRAXoTfrhCfyfSEwJAKLaxBU9eApMeBPB1TkDouZmvPerIClV8lTAd+uF8ZiTaVl69e1FCxQrAd/VTjGw==",
+      "dev": true,
+      "requires": {
+        "performance-now": "^2.1.0"
+      },
+      "dependencies": {
+        "performance-now": {
+          "version": "2.1.0",
+          "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
+          "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=",
+          "dev": true
+        }
+      }
+    },
+    "railroad-diagrams": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/railroad-diagrams/-/railroad-diagrams-1.0.0.tgz",
+      "integrity": "sha1-635iZ1SN3t+4mcG5Dlc3RVnN234=",
+      "dev": true
+    },
+    "randexp": {
+      "version": "0.4.6",
+      "resolved": "https://registry.npmjs.org/randexp/-/randexp-0.4.6.tgz",
+      "integrity": "sha512-80WNmd9DA0tmZrw9qQa62GPPWfuXJknrmVmLcxvq4uZBdYqb1wYoKTmnlGUchvVWe0XiLupYkBoXVOxz3C8DYQ==",
+      "dev": true,
+      "requires": {
+        "discontinuous-range": "1.0.0",
+        "ret": "~0.1.10"
+      }
+    },
     "random-bytes": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz",
@@ -14617,6 +14950,60 @@
         }
       }
     },
+    "react": {
+      "version": "16.4.0",
+      "resolved": "https://registry.npmjs.org/react/-/react-16.4.0.tgz",
+      "integrity": "sha512-K0UrkLXSAekf5nJu89obKUM7o2vc6MMN9LYoKnCa+c+8MJRAT120xzPLENcWSRc7GYKIg0LlgJRDorrufdglQQ==",
+      "dev": true,
+      "requires": {
+        "fbjs": "^0.8.16",
+        "loose-envify": "^1.1.0",
+        "object-assign": "^4.1.1",
+        "prop-types": "^15.6.0"
+      }
+    },
+    "react-dom": {
+      "version": "16.4.0",
+      "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.4.0.tgz",
+      "integrity": "sha512-bbLd+HYpBEnYoNyxDe9XpSG2t9wypMohwQPvKw8Hov3nF7SJiJIgK56b46zHpBUpHb06a1iEuw7G3rbrsnNL6w==",
+      "dev": true,
+      "requires": {
+        "fbjs": "^0.8.16",
+        "loose-envify": "^1.1.0",
+        "object-assign": "^4.1.1",
+        "prop-types": "^15.6.0"
+      }
+    },
+    "react-is": {
+      "version": "16.4.0",
+      "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.4.0.tgz",
+      "integrity": "sha512-8ADZg/mBw+t2Fbr5Hm1K64v8q8Q6E+DprV5wQ5A8PSLW6XP0XJFMdUskVEW8efQ5oUgWHn8EYdHEPAMF0Co6hA==",
+      "dev": true
+    },
+    "react-reconciler": {
+      "version": "0.7.0",
+      "resolved": "https://registry.npmjs.org/react-reconciler/-/react-reconciler-0.7.0.tgz",
+      "integrity": "sha512-50JwZ3yNyMS8fchN+jjWEJOH3Oze7UmhxeoJLn2j6f3NjpfCRbcmih83XTWmzqtar/ivd5f7tvQhvvhism2fgg==",
+      "dev": true,
+      "requires": {
+        "fbjs": "^0.8.16",
+        "loose-envify": "^1.1.0",
+        "object-assign": "^4.1.1",
+        "prop-types": "^15.6.0"
+      }
+    },
+    "react-test-renderer": {
+      "version": "16.4.0",
+      "resolved": "https://registry.npmjs.org/react-test-renderer/-/react-test-renderer-16.4.0.tgz",
+      "integrity": "sha512-Seh1t9xFY6TKiV/hRlPzUkqX1xHOiKIMsctfU0cggo1ajsLjoIJFL520LlrxV+4/VIj+clrCeH6s/aVv/vTStg==",
+      "dev": true,
+      "requires": {
+        "fbjs": "^0.8.16",
+        "object-assign": "^4.1.1",
+        "prop-types": "^15.6.0",
+        "react-is": "^16.4.0"
+      }
+    },
     "read-all-stream": {
       "version": "3.1.0",
       "resolved": "https://registry.npmjs.org/read-all-stream/-/read-all-stream-3.1.0.tgz",
@@ -15158,6 +15545,12 @@
         "onetime": "^1.0.0"
       }
     },
+    "ret": {
+      "version": "0.1.15",
+      "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz",
+      "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==",
+      "dev": true
+    },
     "rgb2hex": {
       "version": "0.1.0",
       "resolved": "https://registry.npmjs.org/rgb2hex/-/rgb2hex-0.1.0.tgz",
@@ -15205,6 +15598,16 @@
       "integrity": "sha1-IPbld0Ih5RkZdjndmsBLe63sls0=",
       "dev": true
     },
+    "rst-selector-parser": {
+      "version": "2.2.3",
+      "resolved": "https://registry.npmjs.org/rst-selector-parser/-/rst-selector-parser-2.2.3.tgz",
+      "integrity": "sha1-gbIw6i/MYGbInjRy3nlChdmwPZE=",
+      "dev": true,
+      "requires": {
+        "lodash.flattendeep": "^4.4.0",
+        "nearley": "^2.7.10"
+      }
+    },
     "run-async": {
       "version": "0.1.0",
       "resolved": "https://registry.npmjs.org/run-async/-/run-async-0.1.0.tgz",
@@ -16889,6 +17292,12 @@
         }
       }
     },
+    "underscore": {
+      "version": "1.4.4",
+      "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.4.4.tgz",
+      "integrity": "sha1-YaajIBBiKvoHljvzJSA88SI51gQ=",
+      "dev": true
+    },
     "uniq": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/uniq/-/uniq-1.0.1.tgz",

+ 5 - 0
package.json

@@ -5,6 +5,7 @@
   "main": "lib/index.js",
   "jsnext:main": "src/index.js",
   "unpkg": "dist/uppy.min.js",
+  "types": "types/index.d.ts",
   "files": [
     "src/",
     "lib/",
@@ -63,6 +64,8 @@
     "chalk": "1.1.3",
     "cssnano": "^3.10.0",
     "disc": "^1.3.3",
+    "enzyme": "^3.3.0",
+    "enzyme-adapter-react-16": "^1.1.1",
     "eslint": "^3.19.0",
     "eslint-config-standard": "^10.2.1",
     "eslint-config-standard-preact": "^1.1.6",
@@ -89,6 +92,8 @@
     "postcss": "^6.0.16",
     "postcss-safe-important": "^1.1.0",
     "pre-commit": "^1.2.2",
+    "react": "^16.4.0",
+    "react-dom": "^16.4.0",
     "redux": "^3.7.2",
     "replace-x": "^1.5.0",
     "sass": "0.5.0",

+ 15 - 8
src/core/Core.js

@@ -2,7 +2,7 @@ const Utils = require('../core/Utils')
 const Translator = require('../core/Translator')
 const ee = require('namespace-emitter')
 const cuid = require('cuid')
-const throttle = require('lodash.throttle')
+// const throttle = require('lodash.throttle')
 const prettyBytes = require('prettier-bytes')
 const match = require('mime-match')
 const DefaultStore = require('../store/DefaultStore')
@@ -179,7 +179,7 @@ class Uppy {
   }
 
   /**
-  * Back compat for when this.state is used instead of this.getState().
+  * Back compat for when uppy.state is used instead of uppy.getState().
   */
   get state () {
     return this.getState()
@@ -189,6 +189,10 @@ class Uppy {
   * Shorthand to set state for a specific file.
   */
   setFileState (fileID, state) {
+    if (!this.getState().files[fileID]) {
+      throw new Error(`Can’t set state for ${fileID} (the file could have been removed)`)
+    }
+
     this.setState({
       files: Object.assign({}, this.getState().files, {
         [fileID]: Object.assign({}, this.getState().files[fileID], state)
@@ -458,7 +462,7 @@ class Uppy {
   }
 
   removeFile (fileID) {
-    const { files, currentUploads } = this.state
+    const { files, currentUploads } = this.getState()
     const updatedFiles = Object.assign({}, files)
     const removedFile = updatedFiles[fileID]
     delete updatedFiles[fileID]
@@ -589,7 +593,8 @@ class Uppy {
 
     this.setState({
       files: {},
-      totalProgress: 0
+      totalProgress: 0,
+      error: null
     })
   }
 
@@ -695,15 +700,17 @@ class Uppy {
     // connection to the remote server. Therefore, we are throtteling them to
     // prevent accessive function calls.
     // see also: https://github.com/tus/tus-js-client/commit/9940f27b2361fd7e10ba58b09b60d82422183bbb
-    const _throttledCalculateProgress = throttle(this._calculateProgress, 100, { leading: true, trailing: true })
+    // const _throttledCalculateProgress = throttle(this._calculateProgress, 100, { leading: true, trailing: true })
 
-    this.on('upload-progress', _throttledCalculateProgress)
+    this.on('upload-progress', this._calculateProgress)
 
     this.on('upload-success', (file, uploadResp, uploadURL) => {
+      const currentProgress = this.getFile(file.id).progress
       this.setFileState(file.id, {
-        progress: Object.assign({}, this.getFile(file.id).progress, {
+        progress: Object.assign({}, currentProgress, {
           uploadComplete: true,
-          percentage: 100
+          percentage: 100,
+          bytesUploaded: currentProgress.bytesTotal
         }),
         uploadURL: uploadURL,
         isPaused: false

+ 71 - 70
src/core/Core.test.js

@@ -160,7 +160,7 @@ describe('src/Core', () => {
         totalProgress: 0
       }
 
-      expect(core.state).toEqual(newState)
+      expect(core.getState()).toEqual(newState)
 
       expect(core.plugins.acquirer[0].mocks.update.mock.calls[1]).toEqual([
         newState
@@ -232,6 +232,7 @@ describe('src/Core', () => {
       capabilities: { resumableUploads: false },
       files: {},
       currentUploads: {},
+      error: null,
       foo: 'bar',
       info: { isHidden: true, message: '', type: 'info' },
       meta: {},
@@ -244,11 +245,11 @@ describe('src/Core', () => {
     const core = new Core()
     const id = core._createUpload([ 'a', 'b' ])
 
-    expect(core.state.currentUploads[id]).toBeDefined()
+    expect(core.getState().currentUploads[id]).toBeDefined()
 
     core.cancelAll()
 
-    expect(core.state.currentUploads[id]).toBeUndefined()
+    expect(core.getState().currentUploads[id]).toBeUndefined()
   })
 
   it('should close, reset and uninstall when the close method is called', () => {
@@ -271,6 +272,7 @@ describe('src/Core', () => {
       capabilities: { resumableUploads: false },
       files: {},
       currentUploads: {},
+      error: null,
       info: { isHidden: true, message: '', type: 'info' },
       meta: {},
       plugins: {},
@@ -338,7 +340,7 @@ describe('src/Core', () => {
 
       return core.upload()
         .then(() => {
-          const fileId = Object.keys(core.state.files)[0]
+          const fileId = Object.keys(core.getState().files)[0]
           expect(preprocessor1.mock.calls.length).toEqual(1)
 
           expect(preprocessor1.mock.calls[0][0].length).toEqual(1)
@@ -358,14 +360,14 @@ describe('src/Core', () => {
         data: new File([sampleImage], { type: 'image/jpeg' })
       })
 
-      const fileId = Object.keys(core.state.files)[0]
+      const fileId = Object.keys(core.getState().files)[0]
       const file = core.getFile(fileId)
       core.emit('preprocess-progress', file, {
         mode: 'determinate',
         message: 'something',
         value: 0
       })
-      expect(core.state.files[fileId].progress).toEqual({
+      expect(core.getFile(fileId).progress).toEqual({
         percentage: 0,
         bytesUploaded: 0,
         bytesTotal: 17175,
@@ -385,14 +387,14 @@ describe('src/Core', () => {
         data: new File([sampleImage], { type: 'image/jpeg' })
       })
 
-      const fileID = Object.keys(core.state.files)[0]
-      const file = core.state.files[fileID]
+      const fileID = Object.keys(core.getState().files)[0]
+      const file = core.getFile(fileID)
       core.emit('preprocess-complete', file, {
         mode: 'determinate',
         message: 'something',
         value: 0
       })
-      expect(core.state.files[fileID].progress).toEqual({
+      expect(core.getFile(fileID).progress).toEqual({
         percentage: 0,
         bytesUploaded: 0,
         bytesTotal: 17175,
@@ -465,14 +467,14 @@ describe('src/Core', () => {
         data: new File([sampleImage], { type: 'image/jpeg' })
       })
 
-      const fileId = Object.keys(core.state.files)[0]
+      const fileId = Object.keys(core.getState().files)[0]
       const file = core.getFile(fileId)
       core.emit('postprocess-progress', file, {
         mode: 'determinate',
         message: 'something',
         value: 0
       })
-      expect(core.state.files[fileId].progress).toEqual({
+      expect(core.getFile(fileId).progress).toEqual({
         percentage: 0,
         bytesUploaded: 0,
         bytesTotal: 17175,
@@ -492,14 +494,14 @@ describe('src/Core', () => {
         data: new File([sampleImage], { type: 'image/jpeg' })
       })
 
-      const fileId = Object.keys(core.state.files)[0]
-      const file = core.state.files[fileId]
+      const fileId = Object.keys(core.getState().files)[0]
+      const file = core.getFile(fileId)
       core.emit('postprocess-complete', file, {
         mode: 'determinate',
         message: 'something',
         value: 0
       })
-      expect(core.state.files[fileId].progress).toEqual({
+      expect(core.getFile(fileId).progress).toEqual({
         percentage: 0,
         bytesUploaded: 0,
         bytesTotal: 17175,
@@ -563,7 +565,7 @@ describe('src/Core', () => {
         data: fileData
       })
 
-      const fileId = Object.keys(core.state.files)[0]
+      const fileId = Object.keys(core.getState().files)[0]
       const newFile = {
         extension: 'jpg',
         id: fileId,
@@ -584,7 +586,7 @@ describe('src/Core', () => {
         source: 'jest',
         type: 'image/jpeg'
       }
-      expect(core.state.files[fileId]).toEqual(newFile)
+      expect(core.getFile(fileId)).toEqual(newFile)
       expect(fileAddedEventMock.mock.calls[0][0]).toEqual(newFile)
     })
 
@@ -621,7 +623,7 @@ describe('src/Core', () => {
         type: 'image/jpeg',
         data: new File([sampleImage], { type: 'image/jpeg' })
       })
-      expect(Object.keys(core.state.files).length).toEqual(0)
+      expect(core.getFiles().length).toEqual(0)
     })
   })
 
@@ -735,8 +737,8 @@ describe('src/Core', () => {
         data: new File([sampleImage], { type: 'image/jpeg' })
       })
 
-      const fileId = Object.keys(core.state.files)[0]
-      expect(Object.keys(core.state.files).length).toEqual(1)
+      const fileId = Object.keys(core.getState().files)[0]
+      expect(core.getFiles().length).toEqual(1)
       core.setState({
         totalProgress: 50
       })
@@ -744,9 +746,9 @@ describe('src/Core', () => {
       const file = core.getFile(fileId)
       core.removeFile(fileId)
 
-      expect(Object.keys(core.state.files).length).toEqual(0)
+      expect(core.getFiles().length).toEqual(0)
       expect(fileRemovedEventMock.mock.calls[0][0]).toEqual(file)
-      expect(core.state.totalProgress).toEqual(0)
+      expect(core.getState().totalProgress).toEqual(0)
     })
   })
 
@@ -767,7 +769,7 @@ describe('src/Core', () => {
         data: new File([sampleImage], { type: 'image/jpeg' })
       })
 
-      const fileId = Object.keys(core.state.files)[0]
+      const fileId = Object.keys(core.getState().files)[0]
       expect(core.getFile(fileId).name).toEqual('foo.jpg')
 
       expect(core.getFile('non existant file')).toEqual(undefined)
@@ -809,7 +811,7 @@ describe('src/Core', () => {
       })
       core.setMeta({ foo: 'bar', bur: 'mur' })
       core.setMeta({ boo: 'moo', bur: 'fur' })
-      expect(core.state.meta).toEqual({
+      expect(core.getState().meta).toEqual({
         foo: 'bar',
         foo2: 'bar2',
         boo: 'moo',
@@ -827,10 +829,10 @@ describe('src/Core', () => {
         data: new File([sampleImage], { type: 'image/jpeg' })
       })
 
-      const fileId = Object.keys(core.state.files)[0]
+      const fileId = Object.keys(core.getState().files)[0]
       core.setFileMeta(fileId, { foo: 'bar', bur: 'mur' })
       core.setFileMeta(fileId, { boo: 'moo', bur: 'fur' })
-      expect(core.state.files[fileId].meta).toEqual({
+      expect(core.getFile(fileId).meta).toEqual({
         name: 'foo.jpg',
         type: 'image/jpeg',
         foo: 'bar',
@@ -852,8 +854,8 @@ describe('src/Core', () => {
         },
         data: new File([sampleImage], { type: 'image/jpeg' })
       })
-      const fileId = Object.keys(core.state.files)[0]
-      expect(core.state.files[fileId].meta).toEqual({
+      const fileId = Object.keys(core.getState().files)[0]
+      expect(core.getFile(fileId).meta).toEqual({
         name: 'foo.jpg',
         type: 'image/jpeg',
         foo2: 'bar2',
@@ -873,13 +875,13 @@ describe('src/Core', () => {
         data: new File([sampleImage], { type: 'image/jpeg' })
       })
 
-      const fileId = Object.keys(core.state.files)[0]
+      const fileId = Object.keys(core.getState().files)[0]
       const file = core.getFile(fileId)
       core._calculateProgress(file, {
         bytesUploaded: 12345,
         bytesTotal: 17175
       })
-      expect(core.state.files[fileId].progress).toEqual({
+      expect(core.getFile(fileId).progress).toEqual({
         percentage: 71,
         bytesUploaded: 12345,
         bytesTotal: 17175,
@@ -891,7 +893,7 @@ describe('src/Core', () => {
         bytesUploaded: 17175,
         bytesTotal: 17175
       })
-      expect(core.state.files[fileId].progress).toEqual({
+      expect(core.getFile(fileId).progress).toEqual({
         percentage: 100,
         bytesUploaded: 17175,
         bytesTotal: 17175,
@@ -916,25 +918,22 @@ describe('src/Core', () => {
         data: new File([sampleImage], { type: 'image/jpeg' })
       })
 
-      const fileId1 = Object.keys(core.state.files)[0]
-      const fileId2 = Object.keys(core.state.files)[1]
-      const file1 = core.state.files[fileId1]
-      const file2 = core.state.files[fileId2]
-      core.state.files[fileId1].progress.uploadStarted = new Date()
-      core.state.files[fileId2].progress.uploadStarted = new Date()
+      const [file1, file2] = core.getFiles()
+      core.setFileState(file1.id, { progress: Object.assign({}, file1.progress, { uploadStarted: new Date() }) })
+      core.setFileState(file2.id, { progress: Object.assign({}, file2.progress, { uploadStarted: new Date() }) })
 
-      core._calculateProgress(file1, {
+      core._calculateProgress(core.getFile(file1.id), {
         bytesUploaded: 12345,
         bytesTotal: 17175
       })
 
-      core._calculateProgress(file2, {
+      core._calculateProgress(core.getFile(file2.id), {
         bytesUploaded: 10201,
         bytesTotal: 17175
       })
 
       core._calculateTotalProgress()
-      expect(core.state.totalProgress).toEqual(65)
+      expect(core.getState().totalProgress).toEqual(65)
     })
 
     it('should reset the progress', () => {
@@ -955,44 +954,41 @@ describe('src/Core', () => {
         data: new File([sampleImage], { type: 'image/jpeg' })
       })
 
-      const fileId1 = Object.keys(core.state.files)[0]
-      const fileId2 = Object.keys(core.state.files)[1]
-      const file1 = core.state.files[fileId1]
-      const file2 = core.state.files[fileId2]
-      core.state.files[fileId1].progress.uploadStarted = new Date()
-      core.state.files[fileId2].progress.uploadStarted = new Date()
+      const [file1, file2] = core.getFiles()
+      core.setFileState(file1.id, { progress: Object.assign({}, file1.progress, { uploadStarted: new Date() }) })
+      core.setFileState(file2.id, { progress: Object.assign({}, file2.progress, { uploadStarted: new Date() }) })
 
-      core._calculateProgress(file1, {
+      core._calculateProgress(core.getFile(file1.id), {
         bytesUploaded: 12345,
         bytesTotal: 17175
       })
 
-      core._calculateProgress(file2, {
+      core._calculateProgress(core.getFile(file2.id), {
         bytesUploaded: 10201,
         bytesTotal: 17175
       })
 
       core._calculateTotalProgress()
 
-      expect(core.state.totalProgress).toEqual(65)
+      expect(core.getState().totalProgress).toEqual(65)
 
       core.resetProgress()
 
-      expect(core.state.files[fileId1].progress).toEqual({
+      expect(core.getFile(file1.id).progress).toEqual({
         percentage: 0,
         bytesUploaded: 0,
         bytesTotal: 17175,
         uploadComplete: false,
         uploadStarted: false
       })
-      expect(core.state.files[fileId2].progress).toEqual({
+      expect(core.getFile(file2.id).progress).toEqual({
         percentage: 0,
         bytesUploaded: 0,
         bytesTotal: 17175,
         uploadComplete: false,
         uploadStarted: false
       })
-      expect(core.state.totalProgress).toEqual(0)
+      expect(core.getState().totalProgress).toEqual(0)
       expect(resetProgressEvent.mock.calls.length).toEqual(1)
     })
   })
@@ -1023,7 +1019,7 @@ describe('src/Core', () => {
         throw new Error('should have thrown')
       } catch (err) {
         expect(err).toMatchObject(new Error('You can only upload 1 file'))
-        expect(core.state.info.message).toEqual('You can only upload 1 file')
+        expect(core.getState().info.message).toEqual('You can only upload 1 file')
       }
     })
 
@@ -1047,7 +1043,7 @@ describe('src/Core', () => {
         throw new Error('should have thrown')
       } catch (err) {
         expect(err).toMatchObject(new Error('You can only upload: image/gif, image/png'))
-        expect(core.state.info.message).toEqual('You can only upload: image/gif, image/png')
+        expect(core.getState().info.message).toEqual('You can only upload: image/gif, image/png')
       }
     })
 
@@ -1069,7 +1065,7 @@ describe('src/Core', () => {
         throw new Error('should have thrown')
       } catch (err) {
         expect(err).toMatchObject(new Error('You can only upload: .gif, .jpg, .jpeg'))
-        expect(core.state.info.message).toEqual('You can only upload: .gif, .jpg, .jpeg')
+        expect(core.getState().info.message).toEqual('You can only upload: .gif, .jpg, .jpeg')
       }
     })
 
@@ -1091,7 +1087,7 @@ describe('src/Core', () => {
         throw new Error('should have thrown')
       } catch (err) {
         expect(err).toMatchObject(new Error('This file exceeds maximum allowed size of 1.2 KB'))
-        expect(core.state.info.message).toEqual('This file exceeds maximum allowed size of 1.2 KB')
+        expect(core.getState().info.message).toEqual('This file exceeds maximum allowed size of 1.2 KB')
       }
     })
   })
@@ -1100,23 +1096,28 @@ describe('src/Core', () => {
     it('should update the state when receiving the error event', () => {
       const core = new Core()
       core.emit('error', new Error('foooooo'))
-      expect(core.state.error).toEqual('foooooo')
+      expect(core.getState().error).toEqual('foooooo')
     })
 
     it('should update the state when receiving the upload-error event', () => {
       const core = new Core()
-      core.state.files['fileId'] = {
-        name: 'filename'
-      }
-      core.emit('upload-error', core.state.files['fileId'], new Error('this is the error'))
-      expect(core.state.info).toEqual({'message': 'Failed to upload filename', 'details': 'this is the error', 'isHidden': false, 'type': 'error'})
+      core.setState({
+        files: {
+          fileId: {
+            id: 'fileId',
+            name: 'filename'
+          }
+        }
+      })
+      core.emit('upload-error', core.getFile('fileId'), new Error('this is the error'))
+      expect(core.getState().info).toEqual({'message': 'Failed to upload filename', 'details': 'this is the error', 'isHidden': false, 'type': 'error'})
     })
 
     it('should reset the error state when receiving the upload event', () => {
       const core = new Core()
       core.emit('error', { foo: 'bar' })
       core.emit('upload')
-      expect(core.state.error).toEqual(null)
+      expect(core.getState().error).toEqual(null)
     })
   })
 
@@ -1174,7 +1175,7 @@ describe('src/Core', () => {
       core.on('info-visible', infoVisibleEvent)
 
       core.info('This is the message', 'info', 0)
-      expect(core.state.info).toEqual({
+      expect(core.getState().info).toEqual({
         isHidden: false,
         type: 'info',
         message: 'This is the message',
@@ -1195,7 +1196,7 @@ describe('src/Core', () => {
           foo: 'bar'
         }
       }, 'warning', 0)
-      expect(core.state.info).toEqual({
+      expect(core.getState().info).toEqual({
         isHidden: false,
         type: 'warning',
         message: 'This is the message',
@@ -1219,7 +1220,7 @@ describe('src/Core', () => {
       expect(infoHiddenEvent.mock.calls.length).toEqual(0)
       setTimeout(() => {
         expect(infoHiddenEvent.mock.calls.length).toEqual(1)
-        expect(core.state.info).toEqual({
+        expect(core.getState().info).toEqual({
           isHidden: true,
           type: 'info',
           message: 'This is the message',
@@ -1241,7 +1242,7 @@ describe('src/Core', () => {
       expect(infoHiddenEvent.mock.calls.length).toEqual(0)
       core.hideInfo()
       expect(infoHiddenEvent.mock.calls.length).toEqual(1)
-      expect(core.state.info).toEqual({
+      expect(core.getState().info).toEqual({
         isHidden: true,
         type: 'info',
         message: 'This is the message',
@@ -1260,15 +1261,15 @@ describe('src/Core', () => {
         data: new File([sampleImage], { type: 'image/jpeg' })
       })
 
-      core._createUpload(Object.keys(core.state.files))
-      const uploadId = Object.keys(core.state.currentUploads)[0]
+      core._createUpload(Object.keys(core.getState().files))
+      const uploadId = Object.keys(core.getState().currentUploads)[0]
       const currentUploadsState = {}
       currentUploadsState[uploadId] = {
-        fileIDs: Object.keys(core.state.files),
+        fileIDs: Object.keys(core.getState().files),
         step: 0,
         result: {}
       }
-      expect(core.state.currentUploads).toEqual(currentUploadsState)
+      expect(core.getState().currentUploads).toEqual(currentUploadsState)
     })
   })
 

+ 3 - 2
src/core/Plugin.js

@@ -44,11 +44,12 @@ module.exports = class Plugin {
   }
 
   getPluginState () {
-    return this.uppy.state.plugins[this.id]
+    const { plugins } = this.uppy.getState()
+    return plugins[this.id]
   }
 
   setPluginState (update) {
-    const plugins = Object.assign({}, this.uppy.state.plugins)
+    const plugins = Object.assign({}, this.uppy.getState().plugins)
     plugins[this.id] = Object.assign({}, plugins[this.id], update)
 
     this.uppy.setState({

+ 10 - 9
src/plugins/AwsS3/Multipart.js

@@ -354,14 +354,6 @@ module.exports = class AwsS3Multipart extends Plugin {
     return Promise.all(promises)
   }
 
-  addResumableUploadsCapabilityFlag () {
-    this.uppy.setState({
-      capabilities: Object.assign({}, this.uppy.getState().capabilities, {
-        resumableUploads: true
-      })
-    })
-  }
-
   onFileRemove (fileID, cb) {
     this.uploaderEvents[fileID].on('file-removed', (file) => {
       if (fileID === file.id) cb(file.id)
@@ -414,11 +406,20 @@ module.exports = class AwsS3Multipart extends Plugin {
   }
 
   install () {
-    this.addResumableUploadsCapabilityFlag()
+    this.uppy.setState({
+      capabilities: Object.assign({}, this.uppy.getState().capabilities, {
+        resumableUploads: true
+      })
+    })
     this.uppy.addUploader(this.upload)
   }
 
   uninstall () {
+    this.uppy.setState({
+      capabilities: Object.assign({}, this.uppy.getState().capabilities, {
+        resumableUploads: false
+      })
+    })
     this.uppy.removeUploader(this.upload)
   }
 }

+ 4 - 2
src/plugins/Dashboard/Dashboard.js

@@ -11,7 +11,7 @@ const { h } = require('preact')
 const PanelContent = (props) => {
   return <div style={{ width: '100%', height: '100%' }}>
     <div class="uppy-DashboardContent-bar">
-      <div class="uppy-DashboardContent-title">
+      <div class="uppy-DashboardContent-title" role="heading" aria-level="h1">
         {props.i18n('importFrom', { name: props.activePanel.name })}
       </div>
       <button class="uppy-DashboardContent-back"
@@ -23,7 +23,7 @@ const PanelContent = (props) => {
 }
 
 const poweredByUppy = (props) => {
-  return <a href="https://uppy.io" rel="noreferrer noopener" target="_blank" class="uppy-Dashboard-poweredBy">Powered by <svg aria-hidden="true" class="UppyIcon uppy-Dashboard-poweredByIcon" width="11" height="11" viewBox="0 0 11 11" xmlns="http://www.w3.org/2000/svg">
+  return <a tabindex="-1" href="https://uppy.io" rel="noreferrer noopener" target="_blank" class="uppy-Dashboard-poweredBy">Powered by <svg aria-hidden="true" class="UppyIcon uppy-Dashboard-poweredByIcon" width="11" height="11" viewBox="0 0 11 11" xmlns="http://www.w3.org/2000/svg">
     <path d="M7.365 10.5l-.01-4.045h2.612L5.5.806l-4.467 5.65h2.604l.01 4.044h3.718z" fill-rule="evenodd" />
   </svg><span class="uppy-Dashboard-poweredByUppy">Uppy</span></a>
 }
@@ -33,6 +33,8 @@ module.exports = function Dashboard (props) {
     { 'uppy-Root': props.isTargetDOMEl },
     'uppy-Dashboard',
     { 'Uppy--isTouchDevice': isTouchDevice() },
+    { 'uppy-Dashboard--animateOpenClose': props.animateOpenClose },
+    { 'uppy-Dashboard--isClosing': props.isClosing },
     { 'uppy-Dashboard--modal': !props.inline },
     { 'uppy-Dashboard--wide': props.isWide }
   )

+ 2 - 2
src/plugins/Dashboard/FileCard.js

@@ -66,11 +66,11 @@ module.exports = class FileCard extends Component {
       <div class="uppy-DashboardFileCard" aria-hidden={!this.props.fileCardFor}>
         <div style={{ width: '100%', height: '100%' }}>
           <div class="uppy-DashboardContent-bar">
-            <h2 class="uppy-DashboardContent-title">
+            <div class="uppy-DashboardContent-title" role="heading" aria-level="h1">
               {this.props.i18nArray('editing', {
                 file: <span class="uppy-DashboardContent-titleFile">{file.meta ? file.meta.name : file.name}</span>
               })}
-            </h2>
+            </div>
             <button class="uppy-DashboardContent-back" type="button" title={this.props.i18n('finishEditingFile')}
               onclick={this.handleSave}>{this.props.i18n('done')}</button>
           </div>

+ 52 - 26
src/plugins/Dashboard/FileItem.js

@@ -9,6 +9,41 @@ const { iconEdit, iconCopy, iconRetry } = require('./icons')
 const classNames = require('classnames')
 const { h } = require('preact')
 
+const FileItemProgressWrapper = (props) => {
+  if (props.hideRetryButton && props.error) {
+    return
+  }
+
+  if (props.isUploaded ||
+      props.bundled ||
+      (props.hidePauseResumeCancelButtons && !props.error)) {
+    return <div class="uppy-DashboardItem-progressIndicator">
+      <FileItemProgress
+        progress={props.file.progress.percentage}
+        fileID={props.file.id}
+        hidePauseResumeCancelButtons={props.hidePauseResumeCancelButtons}
+        bundled={props.bundled}
+      />
+    </div>
+  }
+
+  return <button
+    class="uppy-DashboardItem-progressIndicator"
+    type="button"
+    aria-label={props.progressIndicatorTitle}
+    title={props.progressIndicatorTitle}
+    onclick={props.onPauseResumeCancelRetry}>
+    {props.error
+      ? props.hideRetryButton ? null : iconRetry()
+      : <FileItemProgress
+        progress={props.file.progress.percentage}
+        fileID={props.file.id}
+        hidePauseResumeCancelButtons={props.hidePauseResumeCancelButtons}
+      />
+    }
+  </button>
+}
+
 module.exports = function fileItem (props) {
   const file = props.file
   const acquirers = props.acquirers
@@ -25,13 +60,19 @@ module.exports = function fileItem (props) {
 
   const onPauseResumeCancelRetry = (ev) => {
     if (isUploaded) return
+
     if (error && !props.hideRetryButton) {
       props.retryUpload(file.id)
       return
     }
+
+    if (props.hidePauseResumeCancelButtons) {
+      return
+    }
+
     if (props.resumableUploads) {
       props.pauseUpload(file.id)
-    } else if (!props.hideCancelButton) {
+    } else {
       props.cancelUpload(file.id)
     }
   }
@@ -43,7 +84,8 @@ module.exports = function fileItem (props) {
     { 'is-complete': isUploaded },
     { 'is-paused': isPaused },
     { 'is-error': error },
-    { 'is-resumable': props.resumableUploads }
+    { 'is-resumable': props.resumableUploads },
+    { 'is-bundled': props.bundledUpload }
   )
 
   const progressIndicatorTitle = isUploaded
@@ -66,39 +108,23 @@ module.exports = function fileItem (props) {
         <FilePreview file={file} />
       </div>
       <div class="uppy-DashboardItem-progress">
-        {isUploaded
-          ? <div class="uppy-DashboardItem-progressIndicator">
-            {FileItemProgress({
-              progress: file.progress.percentage,
-              fileID: file.id
-            })}
-          </div>
-          : <button class="uppy-DashboardItem-progressIndicator"
-            type="button"
-            aria-label={progressIndicatorTitle}
-            title={progressIndicatorTitle}
-            onclick={onPauseResumeCancelRetry}>
-            {error
-              ? props.hideCancelButton ? null : iconRetry()
-              : FileItemProgress({
-                progress: file.progress.percentage,
-                fileID: file.id,
-                hideCancelButton: props.hideCancelButton
-              })
-            }
-          </button>
-        }
+        <FileItemProgressWrapper
+          progressIndicatorTitle={progressIndicatorTitle}
+          onPauseResumeCancelRetry={onPauseResumeCancelRetry}
+          file={file}
+          error={error}
+          {...props} />
       </div>
     </div>
     <div class="uppy-DashboardItem-info">
-      <h4 class="uppy-DashboardItem-name" title={fileName}>
+      <div class="uppy-DashboardItem-name" title={fileName}>
         {props.showLinkToFileUploadResult && file.uploadURL
           ? <a href={file.uploadURL} rel="noreferrer noopener" target="_blank">
             {file.extension ? truncatedFileName + '.' + file.extension : truncatedFileName}
           </a>
           : file.extension ? truncatedFileName + '.' + file.extension : truncatedFileName
         }
-      </h4>
+      </div>
       <div class="uppy-DashboardItem-status">
         {file.data.size ? <div class="uppy-DashboardItem-statusSize">{prettyBytes(file.data.size)}</div> : null}
         {file.source && <div class="uppy-DashboardItem-sourceIcon">

+ 13 - 10
src/plugins/Dashboard/FileItemProgress.js

@@ -13,21 +13,24 @@ module.exports = (props) => {
   return (
     <svg width="70" height="70" viewBox="0 0 36 36" class="UppyIcon UppyIcon-progressCircle">
       <g class="progress-group">
-        <circle r="15" cx="18" cy="18" stroke-width="2" fill="none" class="bg" />
-        <circle r="15" cx="18" cy="18" transform="rotate(-90, 18, 18)" stroke-width="2" fill="none" class="progress"
+        <circle class="bg" r="15" cx="18" cy="18" stroke-width="2" fill="none" />
+        <circle class="progress" r="15" cx="18" cy="18" transform="rotate(-90, 18, 18)" stroke-width="2" fill="none"
           stroke-dasharray={circleLength}
           stroke-dashoffset={circleLength - (circleLength / 100 * props.progress)}
         />
       </g>
-      <polygon transform="translate(3, 3)" points="12 20 12 10 20 15" class="play" />
-      <g transform="translate(14.5, 13)" class="pause">
-        <rect x="0" y="0" width="2" height="10" rx="0" />
-        <rect x="5" y="0" width="2" height="10" rx="0" />
-      </g>
-      <polygon transform="translate(2, 3)" points="14 22.5 7 15.2457065 8.99985857 13.1732815 14 18.3547104 22.9729883 9 25 11.1005634" class="check" />
-      {props.hideCancelButton ? null
-        : <polygon class="cancel" transform="translate(2, 2)" points="19.8856516 11.0625 16 14.9481516 12.1019737 11.0625 11.0625 12.1143484 14.9481516 16 11.0625 19.8980263 12.1019737 20.9375 16 17.0518484 19.8856516 20.9375 20.9375 19.8980263 17.0518484 16 20.9375 12" />
+      {!props.hidePauseResumeCancelButtons && !props.bundled ? (
+        <g>
+          <polygon class="play" transform="translate(3, 3)" points="12 20 12 10 20 15" />
+          <g class="pause" transform="translate(14.5, 13)">
+            <rect x="0" y="0" width="2" height="10" rx="0" />
+            <rect x="5" y="0" width="2" height="10" rx="0" />
+          </g>
+          <polygon class="cancel" transform="translate(2, 2)" points="19.8856516 11.0625 16 14.9481516 12.1019737 11.0625 11.0625 12.1143484 14.9481516 16 11.0625 19.8980263 12.1019737 20.9375 16 17.0518484 19.8856516 20.9375 20.9375 19.8980263 17.0518484 16 20.9375 12" />
+        </g>
+        ) : null
       }
+      <polygon class="check" transform="translate(2, 3)" points="14 22.5 7 15.2457065 8.99985857 13.1732815 14 18.3547104 22.9729883 9 25 11.1005634" />
     </svg>
   )
 }

+ 2 - 1
src/plugins/Dashboard/FileList.js

@@ -40,9 +40,10 @@ module.exports = (props) => {
         pauseUpload={props.pauseUpload}
         cancelUpload={props.cancelUpload}
         retryUpload={props.retryUpload}
-        hideCancelButton={props.hideCancelButton}
+        hidePauseResumeCancelButtons={props.hidePauseResumeCancelButtons}
         hideRetryButton={props.hideRetryButton}
         resumableUploads={props.resumableUploads}
+        bundled={props.bundled}
         isWide={props.isWide}
         showLinkToFileUploadResult={props.showLinkToFileUploadResult}
         metaFields={props.metaFields}

+ 1 - 1
src/plugins/Dashboard/Tabs.js

@@ -66,7 +66,7 @@ class Tabs extends Component {
               aria-selected={this.props.activePanel.id === target.id}
               onclick={() => this.props.showPanel(target.id)}>
               {target.icon()}
-              <h5 class="uppy-DashboardTab-name">{target.name}</h5>
+              <div class="uppy-DashboardTab-name">{target.name}</div>
             </button>
           </li>
         })}

+ 81 - 50
src/plugins/Dashboard/index.js

@@ -13,19 +13,22 @@ const { defaultTabIcon } = require('./icons')
 // MIT licence, https://github.com/ghosh/micromodal/blob/master/LICENSE.md
 // Copyright (c) 2017 Indrashish Ghosh
 const FOCUSABLE_ELEMENTS = [
-  'a[href]',
-  'area[href]',
-  'input:not([disabled]):not([type="hidden"]):not([aria-hidden])',
-  'select:not([disabled]):not([aria-hidden])',
-  'textarea:not([disabled]):not([aria-hidden])',
-  'button:not([disabled]):not([aria-hidden])',
-  'iframe',
-  'object',
-  'embed',
-  '[contenteditable]',
-  '[tabindex]:not([tabindex^="-"])'
+  'a[href]:not([tabindex^="-"]):not([inert]):not([aria-hidden])',
+  'area[href]:not([tabindex^="-"]):not([inert]):not([aria-hidden])',
+  'input:not([disabled]):not([inert]):not([aria-hidden])',
+  'select:not([disabled]):not([inert]):not([aria-hidden])',
+  'textarea:not([disabled]):not([inert]):not([aria-hidden])',
+  'button:not([disabled]):not([inert]):not([aria-hidden])',
+  'iframe:not([tabindex^="-"]):not([inert]):not([aria-hidden])',
+  'object:not([tabindex^="-"]):not([inert]):not([aria-hidden])',
+  'embed:not([tabindex^="-"]):not([inert]):not([aria-hidden])',
+  '[contenteditable]:not([tabindex^="-"]):not([inert]):not([aria-hidden])',
+  '[tabindex]:not([tabindex^="-"]):not([inert]):not([aria-hidden])'
 ]
 
+const TAB_KEY = 9
+const ESC_KEY = 27
+
 /**
  * Dashboard UI with previews, metadata editing, tabs for various services and more
  */
@@ -98,7 +101,7 @@ module.exports = class Dashboard extends Plugin {
       showProgressDetails: false,
       hideUploadButton: false,
       hideRetryButton: false,
-      hideCancelButton: false,
+      hidePauseResumeCancelButtons: false,
       hideProgressAfterFinish: false,
       note: null,
       closeModalOnClickOutside: false,
@@ -106,6 +109,7 @@ module.exports = class Dashboard extends Plugin {
       disableInformer: false,
       disableThumbnailGenerator: false,
       disablePageScrollWhenModalOpen: true,
+      animateOpenClose: true,
       proudlyDisplayPoweredByUppy: true,
       onRequestCloseModal: () => this.closeModal(),
       locale: defaultLocale
@@ -242,23 +246,43 @@ module.exports = class Dashboard extends Plugin {
     this.savedActiveElement = document.activeElement
 
     if (this.opts.disablePageScrollWhenModalOpen) {
-      document.body.classList.add('uppy-Dashboard-isOpen')
+      document.body.classList.add('uppy-Dashboard-isFixed')
     }
 
+    // handle ESC and TAB keys in modal dialog
+    document.addEventListener('keydown', this.onKeydown)
+
     this.rerender(this.uppy.getState())
     this.updateDashboardElWidth()
     this.setFocusToBrowse()
   }
 
   closeModal () {
-    this.setPluginState({
-      isHidden: true
-    })
-
     if (this.opts.disablePageScrollWhenModalOpen) {
-      document.body.classList.remove('uppy-Dashboard-isOpen')
+      document.body.classList.remove('uppy-Dashboard-isFixed')
+    }
+
+    if (this.opts.animateOpenClose) {
+      this.setPluginState({
+        isClosing: true
+      })
+      const handler = () => {
+        this.setPluginState({
+          isHidden: true,
+          isClosing: false
+        })
+        this.el.removeEventListener('animationend', handler, false)
+      }
+      this.el.addEventListener('animationend', handler, false)
+    } else {
+      this.setPluginState({
+        isHidden: true
+      })
     }
 
+    // handle ESC and TAB keys in modal dialog
+    document.removeEventListener('keydown', this.onKeydown)
+
     this.savedActiveElement.focus()
   }
 
@@ -268,9 +292,9 @@ module.exports = class Dashboard extends Plugin {
 
   onKeydown (event) {
     // close modal on esc key press
-    if (event.keyCode === 27) this.requestCloseModal(event)
+    if (event.keyCode === ESC_KEY) this.requestCloseModal(event)
     // maintainFocus on tab key press
-    if (event.keyCode === 9) this.maintainFocus(event)
+    if (event.keyCode === TAB_KEY) this.maintainFocus(event)
   }
 
   handleClickOutside () {
@@ -289,12 +313,16 @@ module.exports = class Dashboard extends Plugin {
         return
       }
       this.uppy.log('[Dashboard] File pasted')
-      this.uppy.addFile({
-        source: this.id,
-        name: file.name,
-        type: file.type,
-        data: blob
-      })
+      try {
+        this.uppy.addFile({
+          source: this.id,
+          name: file.name,
+          type: file.type,
+          data: blob
+        })
+      } catch (err) {
+        // Nothing, restriction errors handled in Core
+      }
     })
   }
 
@@ -303,12 +331,16 @@ module.exports = class Dashboard extends Plugin {
     const files = toArray(ev.target.files)
 
     files.forEach((file) => {
-      this.uppy.addFile({
-        source: this.id,
-        name: file.name,
-        type: file.type,
-        data: file
-      })
+      try {
+        this.uppy.addFile({
+          source: this.id,
+          name: file.name,
+          type: file.type,
+          data: file
+        })
+      } catch (err) {
+        // Nothing, restriction errors handled in Core
+      }
     })
   }
 
@@ -323,10 +355,6 @@ module.exports = class Dashboard extends Plugin {
       this.uppy.log('Dashboard modal trigger not found. Make sure `trigger` is set in Dashboard options unless you are planning to call openModal() method yourself')
     }
 
-    if (!this.opts.inline) {
-      document.addEventListener('keydown', this.onKeydown)
-    }
-
     // Drag Drop
     this.removeDragDropListener = dragDrop(this.el, (files) => {
       this.handleDrop(files)
@@ -342,10 +370,6 @@ module.exports = class Dashboard extends Plugin {
       showModalTrigger.forEach(trigger => trigger.removeEventListener('click', this.openModal))
     }
 
-    if (!this.opts.inline) {
-      document.removeEventListener('keydown', this.onKeydown)
-    }
-
     this.removeDragDropListener()
     window.removeEventListener('resize', this.updateDashboardElWidth)
   }
@@ -369,18 +393,22 @@ module.exports = class Dashboard extends Plugin {
     this.uppy.log('[Dashboard] Files were dropped')
 
     files.forEach((file) => {
-      this.uppy.addFile({
-        source: this.id,
-        name: file.name,
-        type: file.type,
-        data: file
-      })
+      try {
+        this.uppy.addFile({
+          source: this.id,
+          name: file.name,
+          type: file.type,
+          data: file
+        })
+      } catch (err) {
+        // Nothing, restriction errors handled in Core
+      }
     })
   }
 
   render (state) {
     const pluginState = this.getPluginState()
-    const files = state.files
+    const { files, capabilities } = state
 
     const newFiles = Object.keys(files).filter((file) => {
       return !files[file].progress.uploadStarted
@@ -456,12 +484,14 @@ module.exports = class Dashboard extends Plugin {
       totalProgress: state.totalProgress,
       acquirers: acquirers,
       activePanel: pluginState.activePanel,
+      animateOpenClose: this.opts.animateOpenClose,
+      isClosing: pluginState.isClosing,
       getPlugin: this.uppy.getPlugin,
       progressindicators: progressindicators,
       autoProceed: this.uppy.opts.autoProceed,
       hideUploadButton: this.opts.hideUploadButton,
       hideRetryButton: this.opts.hideRetryButton,
-      hideCancelButton: this.opts.hideCancelButton,
+      hidePauseResumeCancelButtons: this.opts.hidePauseResumeCancelButtons,
       id: this.id,
       closeModal: this.requestCloseModal,
       handleClickOutside: this.handleClickOutside,
@@ -478,7 +508,8 @@ module.exports = class Dashboard extends Plugin {
       info: this.uppy.info,
       note: this.opts.note,
       metaFields: pluginState.metaFields,
-      resumableUploads: this.uppy.state.capabilities.resumableUploads || false,
+      resumableUploads: capabilities.resumableUploads || false,
+      bundled: capabilities.bundled || false,
       startUpload: startUpload,
       pauseUpload: this.uppy.pauseResume,
       retryUpload: this.uppy.retryUpload,
@@ -534,7 +565,7 @@ module.exports = class Dashboard extends Plugin {
         target: this,
         hideUploadButton: this.opts.hideUploadButton,
         hideRetryButton: this.opts.hideRetryButton,
-        hideCancelButton: this.opts.hideCancelButton,
+        hidePauseResumeCancelButtons: this.opts.hidePauseResumeCancelButtons,
         showProgressDetails: this.opts.showProgressDetails,
         hideAfterFinish: this.opts.hideProgressAfterFinish,
         locale: this.opts.locale

+ 20 - 12
src/plugins/DragDrop/index.js

@@ -79,12 +79,16 @@ module.exports = class DragDrop extends Plugin {
     this.uppy.log('[DragDrop] Files dropped')
 
     files.forEach((file) => {
-      this.uppy.addFile({
-        source: this.id,
-        name: file.name,
-        type: file.type,
-        data: file
-      })
+      try {
+        this.uppy.addFile({
+          source: this.id,
+          name: file.name,
+          type: file.type,
+          data: file
+        })
+      } catch (err) {
+        // Nothing, restriction errors handled in Core
+      }
     })
   }
 
@@ -94,12 +98,16 @@ module.exports = class DragDrop extends Plugin {
     const files = toArray(ev.target.files)
 
     files.forEach((file) => {
-      this.uppy.addFile({
-        source: this.id,
-        name: file.name,
-        type: file.type,
-        data: file
-      })
+      try {
+        this.uppy.addFile({
+          source: this.id,
+          name: file.name,
+          type: file.type,
+          data: file
+        })
+      } catch (err) {
+        // Nothing, restriction errors handled in Core
+      }
     })
   }
 

+ 5 - 1
src/plugins/Dummy.js

@@ -35,7 +35,11 @@ module.exports = class Dummy extends Plugin {
       data: blob
     }
     this.props.log('Adding fake file blob')
-    this.props.addFile(file)
+    try {
+      this.props.addFile(file)
+    } catch (err) {
+      // Nothing, restriction errors handled in Core
+    }
   }
 
   render (state) {

+ 10 - 6
src/plugins/FileInput.js

@@ -45,12 +45,16 @@ module.exports = class FileInput extends Plugin {
     const files = toArray(ev.target.files)
 
     files.forEach((file) => {
-      this.uppy.addFile({
-        source: this.id,
-        name: file.name,
-        type: file.type,
-        data: file
-      })
+      try {
+        this.uppy.addFile({
+          source: this.id,
+          name: file.name,
+          type: file.type,
+          data: file
+        })
+      } catch (err) {
+        // Nothing, restriction errors handled in Core
+      }
     })
   }
 

+ 8 - 9
src/plugins/GoldenRetriever/index.js

@@ -65,11 +65,9 @@ module.exports = class GoldenRetriever extends Plugin {
   getWaitingFiles () {
     const waitingFiles = {}
 
-    const allFiles = this.uppy.state.files
-    Object.keys(allFiles).forEach((fileID) => {
-      const file = this.uppy.getFile(fileID)
+    this.uppy.getFiles().forEach((file) => {
       if (!file.progress || !file.progress.uploadStarted) {
-        waitingFiles[fileID] = file
+        waitingFiles[file.id] = file
       }
     })
 
@@ -84,7 +82,7 @@ module.exports = class GoldenRetriever extends Plugin {
   getUploadingFiles () {
     const uploadingFiles = {}
 
-    const { currentUploads } = this.uppy.state
+    const { currentUploads } = this.uppy.getState()
     if (currentUploads) {
       const uploadIDs = Object.keys(currentUploads)
       uploadIDs.forEach((uploadID) => {
@@ -112,8 +110,9 @@ module.exports = class GoldenRetriever extends Plugin {
       Object.assign(pluginData, data)
     })
 
+    const { currentUploads } = this.uppy.getState()
     this.MetaDataStore.save({
-      currentUploads: this.uppy.state.currentUploads,
+      currentUploads: currentUploads,
       files: filesToSave,
       pluginData: pluginData
     })
@@ -122,7 +121,7 @@ module.exports = class GoldenRetriever extends Plugin {
   loadFileBlobsFromServiceWorker () {
     this.ServiceWorkerStore.list().then((blobs) => {
       const numberOfFilesRecovered = Object.keys(blobs).length
-      const numberOfFilesTryingToRecover = Object.keys(this.uppy.state.files).length
+      const numberOfFilesTryingToRecover = this.uppy.getFiles().length
       if (numberOfFilesRecovered === numberOfFilesTryingToRecover) {
         this.uppy.log(`[GoldenRetriever] Successfully recovered ${numberOfFilesRecovered} blobs from Service Worker!`)
         this.uppy.info(`Successfully recovered ${numberOfFilesRecovered} files`, 'success', 3000)
@@ -154,7 +153,7 @@ module.exports = class GoldenRetriever extends Plugin {
 
   onBlobsLoaded (blobs) {
     const obsoleteBlobs = []
-    const updatedFiles = Object.assign({}, this.uppy.state.files)
+    const updatedFiles = Object.assign({}, this.uppy.getState().files)
     Object.keys(blobs).forEach((fileID) => {
       const originalFile = this.uppy.getFile(fileID)
       if (!originalFile) {
@@ -204,7 +203,7 @@ module.exports = class GoldenRetriever extends Plugin {
   install () {
     this.loadFilesStateFromLocalStorage()
 
-    if (Object.keys(this.uppy.state.files).length > 0) {
+    if (this.uppy.getFiles().length > 0) {
       if (this.ServiceWorkerStore) {
         this.uppy.log('[GoldenRetriever] Attempting to load files from Service Worker...')
         this.loadFileBlobsFromServiceWorker()

+ 4 - 4
src/plugins/ReduxDevTools.js

@@ -40,13 +40,13 @@ module.exports = class ReduxDevTools extends Plugin {
             return
           case 'IMPORT_STATE':
             const computedStates = message.payload.nextLiftedState.computedStates
-            this.uppy.state = Object.assign({}, this.uppy.state, computedStates[computedStates.length - 1].state)
-            this.uppy.updateAll(this.uppy.state)
+            this.uppy.store.state = Object.assign({}, this.uppy.getState(), computedStates[computedStates.length - 1].state)
+            this.uppy.updateAll(this.uppy.getState())
             return
           case 'JUMP_TO_STATE':
           case 'JUMP_TO_ACTION':
-            this.uppy.store.state = Object.assign({}, this.uppy.state, JSON.parse(message.state))
-            this.uppy.updateAll(this.uppy.state)
+            this.uppy.store.state = Object.assign({}, this.uppy.getState(), JSON.parse(message.state))
+            this.uppy.updateAll(this.uppy.getState())
         }
       }
     })

+ 9 - 8
src/plugins/StatusBar/StatusBar.js

@@ -100,7 +100,7 @@ module.exports = (props) => {
       <div class="uppy-StatusBar-actions">
         { props.newFiles && !props.hideUploadButton ? <UploadBtn {...props} uploadState={uploadState} /> : null }
         { props.error && !props.hideRetryButton ? <RetryBtn {...props} /> : null }
-        { !props.hideCancelButton && uploadState !== statusBarStates.STATE_WAITING && uploadState !== statusBarStates.STATE_COMPLETE
+        { !props.hidePauseResumeCancelButtons && uploadState !== statusBarStates.STATE_WAITING && uploadState !== statusBarStates.STATE_COMPLETE
           ? <CancelBtn {...props} />
           : null
         }
@@ -116,7 +116,6 @@ const UploadBtn = (props) => {
     'uppy-StatusBar-actionBtn',
     'uppy-StatusBar-actionBtn--upload',
     { 'uppy-c-btn-primary': props.uploadState === statusBarStates.STATE_WAITING }
-    // { 'uppy-StatusBar-actionBtn uppy-StatusBar-actionBtn--upload': props.uploadState !== statusBarStates.STATE_WAITING }
   )
 
   return <button type="button"
@@ -161,7 +160,7 @@ const PauseResumeButtons = (props) => {
         : <svg aria-hidden="true" class="UppyIcon" width="16" height="17" viewBox="0 0 12 13">
           <path d="M4.888.81v11.38c0 .446-.324.81-.722.81H2.722C2.324 13 2 12.636 2 12.19V.81c0-.446.324-.81.722-.81h1.444c.398 0 .722.364.722.81zM9.888.81v11.38c0 .446-.324.81-.722.81H7.722C7.324 13 7 12.636 7 12.19V.81c0-.446.324-.81.722-.81h1.444c.398 0 .722.364.722.81z" />
         </svg>
-      : props.hideCancelButton ? null : <svg aria-hidden="true" class="UppyIcon" width="16px" height="16px" viewBox="0 0 19 19">
+      : <svg aria-hidden="true" class="UppyIcon" width="16px" height="16px" viewBox="0 0 19 19">
         <path d="M17.318 17.232L9.94 9.854 9.586 9.5l-.354.354-7.378 7.378h.707l-.62-.62v.706L9.318 9.94l.354-.354-.354-.354L1.94 1.854v.707l.62-.62h-.706l7.378 7.378.354.354.354-.354 7.378-7.378h-.707l.622.62v-.706L9.854 9.232l-.354.354.354.354 7.378 7.378.708-.707-7.38-7.378v.708l7.38-7.38.353-.353-.353-.353-.622-.622-.353-.353-.354.352-7.378 7.38h.708L2.56 1.23 2.208.88l-.353.353-.622.62-.353.355.352.353 7.38 7.38v-.708l-7.38 7.38-.353.353.352.353.622.622.353.353.354-.353 7.38-7.38h-.708l7.38 7.38z" />
       </svg>
     }
@@ -192,11 +191,13 @@ const ProgressBarUploading = (props) => {
     return null
   }
 
+  const title = props.isAllPaused ? props.i18n('paused') : props.i18n('uploading')
+
   return (
-    <div class="uppy-StatusBar-content" title={props.isAllPaused ? props.i18n('paused') : props.i18n('uploading')}>
-      { <PauseResumeButtons {...props} /> }
+    <div class="uppy-StatusBar-content" aria-label={title} title={title}>
+      { !props.hidePauseResumeCancelButtons && <PauseResumeButtons {...props} /> }
       <div class="uppy-StatusBar-status">
-        <span class="uppy-StatusBar-statusPrimary">{ props.isAllPaused ? props.i18n('paused') : props.i18n('uploading') }: {props.totalProgress}%</span>
+        <span class="uppy-StatusBar-statusPrimary">{title}: {props.totalProgress}%</span>
         <br />
         { !props.isAllPaused && <ThrottledProgressDetails {...props} /> }
       </div>
@@ -215,11 +216,11 @@ const ProgressBarComplete = ({ totalProgress, i18n }) => {
   )
 }
 
-const ProgressBarError = ({ error, retryAll, i18n }) => {
+const ProgressBarError = ({ error, retryAll, hideRetryButton, i18n }) => {
   return (
     <div class="uppy-StatusBar-content" role="alert">
       <strong class="uppy-StatusBar-contentPadding">{i18n('uploadFailed')}.</strong>
-      <span class="uppy-StatusBar-contentPadding">{i18n('pleasePressRetry')}</span>
+      { !hideRetryButton && <span class="uppy-StatusBar-contentPadding">{i18n('pleasePressRetry')}</span> }
       <span class="uppy-StatusBar-details"
         aria-label={error}
         data-microtip-position="top"

+ 2 - 2
src/plugins/StatusBar/index.js

@@ -55,7 +55,7 @@ module.exports = class StatusBar extends Plugin {
       target: 'body',
       hideUploadButton: false,
       hideRetryButton: false,
-      hideCancelButton: false,
+      hidePauseResumeCancelButtons: false,
       showProgressDetails: false,
       locale: defaultLocale,
       hideAfterFinish: true
@@ -220,7 +220,7 @@ module.exports = class StatusBar extends Plugin {
       showProgressDetails: this.opts.showProgressDetails,
       hideUploadButton: this.opts.hideUploadButton,
       hideRetryButton: this.opts.hideRetryButton,
-      hideCancelButton: this.opts.hideCancelButton,
+      hidePauseResumeCancelButtons: this.opts.hidePauseResumeCancelButtons,
       hideAfterFinish: this.opts.hideAfterFinish
     })
   }

+ 2 - 7
src/plugins/ThumbnailGenerator/index.js

@@ -153,13 +153,8 @@ module.exports = class ThumbnailGenerator extends Plugin {
    * Set the preview URL for a file.
    */
   setPreviewURL (fileID, preview) {
-    const { files } = this.uppy.state
-    this.uppy.setState({
-      files: Object.assign({}, files, {
-        [fileID]: Object.assign({}, files[fileID], {
-          preview: preview
-        })
-      })
+    this.uppy.setFileState(fileID, {
+      preview: preview
     })
   }
 

+ 4 - 4
src/plugins/ThumbnailGenerator/index.test.js

@@ -186,13 +186,13 @@ describe('uploader/ThumbnailGeneratorPlugin', () => {
             }
           }
         },
-        setState: jest.fn()
+        setFileState: jest.fn()
       }
       const plugin = new ThumbnailGeneratorPlugin(core)
       plugin.setPreviewURL('file1', 'moo')
-      expect(core.setState).toHaveBeenCalledTimes(1)
-      expect(core.setState).toHaveBeenCalledWith({
-        files: { file1: { preview: 'moo' }, file2: { preview: 'boo' } }
+      expect(core.setFileState).toHaveBeenCalledTimes(1)
+      expect(core.setFileState).toHaveBeenCalledWith('file1', {
+        preview: 'moo'
       })
     })
   })

+ 11 - 16
src/plugins/Transloadit/index.js

@@ -216,7 +216,7 @@ module.exports = class Transloadit extends Plugin {
         return newFile
       }
 
-      const files = Object.assign({}, this.uppy.state.files)
+      const files = Object.assign({}, this.uppy.getState().files)
       fileIDs.forEach((id) => {
         files[id] = attachAssemblyMetadata(files[id], assembly)
       })
@@ -272,23 +272,21 @@ module.exports = class Transloadit extends Plugin {
   }
 
   findFile (uploadedFile) {
-    const files = this.uppy.state.files
-    for (const id in files) {
-      if (!files.hasOwnProperty(id)) {
-        continue
-      }
+    const files = this.uppy.getFiles()
+    for (let i = 0; i < files.length; i++) {
+      const file = files[i]
       // Completed file upload.
-      if (files[id].uploadURL === uploadedFile.tus_upload_url) {
-        return files[id]
+      if (file.uploadURL === uploadedFile.tus_upload_url) {
+        return file
       }
       // In-progress file upload.
-      if (files[id].tus && files[id].tus.uploadUrl === uploadedFile.tus_upload_url) {
-        return files[id]
+      if (file.tus && file.tus.uploadUrl === uploadedFile.tus_upload_url) {
+        return file
       }
       if (!uploadedFile.is_tus_file) {
         // Fingers-crossed check for non-tus uploads, eg imported from S3.
-        if (files[id].name === uploadedFile.name && files[id].size === uploadedFile.size) {
-          return files[id]
+        if (file.name === uploadedFile.name && file.size === uploadedFile.size) {
+          return file
         }
       }
     }
@@ -852,10 +850,7 @@ module.exports = class Transloadit extends Plugin {
   }
 
   getAssemblyFiles (assemblyID) {
-    const fileIDs = Object.keys(this.uppy.state.files)
-    return fileIDs.map((fileID) => {
-      return this.uppy.getFile(fileID)
-    }).filter((file) => {
+    return this.uppy.getFiles().filter((file) => {
       return file && file.transloadit && file.transloadit.assembly === assemblyID
     })
   }

+ 11 - 18
src/plugins/Tus.js

@@ -85,7 +85,7 @@ module.exports = class Tus extends Plugin {
   }
 
   handleResetProgress () {
-    const files = Object.assign({}, this.uppy.state.files)
+    const files = Object.assign({}, this.uppy.getState().files)
     Object.keys(files).forEach((fileID) => {
       // Only clone the file object if it has a Tus `uploadUrl` attached.
       if (files[fileID].tus && files[fileID].tus.uploadUrl) {
@@ -346,13 +346,6 @@ module.exports = class Tus extends Plugin {
     })
   }
 
-  updateFile (file) {
-    const files = Object.assign({}, this.uppy.state.files, {
-      [file.id]: file
-    })
-    this.uppy.setState({ files })
-  }
-
   onReceiveUploadUrl (file, uploadURL) {
     const currentFile = this.uppy.getFile(file.id)
     if (!currentFile) return
@@ -360,12 +353,11 @@ module.exports = class Tus extends Plugin {
     // or resume: false in options
     if ((!currentFile.tus || currentFile.tus.uploadUrl !== uploadURL) && this.opts.resume) {
       this.uppy.log('[Tus] Storing upload url')
-      const newFile = Object.assign({}, currentFile, {
+      this.uppy.setFileState(currentFile.id, {
         tus: Object.assign({}, currentFile.tus, {
           uploadUrl: uploadURL
         })
       })
-      this.updateFile(newFile)
     }
   }
 
@@ -459,16 +451,12 @@ module.exports = class Tus extends Plugin {
       .then(() => null)
   }
 
-  addResumableUploadsCapabilityFlag () {
-    const newCapabilities = Object.assign({}, this.uppy.getState().capabilities)
-    newCapabilities.resumableUploads = true
+  install () {
     this.uppy.setState({
-      capabilities: newCapabilities
+      capabilities: Object.assign({}, this.uppy.getState().capabilities, {
+        resumableUploads: true
+      })
     })
-  }
-
-  install () {
-    this.addResumableUploadsCapabilityFlag()
     this.uppy.addUploader(this.handleUpload)
 
     this.uppy.on('reset-progress', this.handleResetProgress)
@@ -479,6 +467,11 @@ module.exports = class Tus extends Plugin {
   }
 
   uninstall () {
+    this.uppy.setState({
+      capabilities: Object.assign({}, this.uppy.getState().capabilities, {
+        resumableUploads: false
+      })
+    })
     this.uppy.removeUploader(this.handleUpload)
 
     if (this.opts.autoRetry) {

+ 5 - 1
src/plugins/Url/index.js

@@ -135,7 +135,11 @@ module.exports = class Url extends Plugin {
       })
       .then((tagFile) => {
         this.uppy.log('[Url] Adding remote file')
-        return this.uppy.addFile(tagFile)
+        try {
+          this.uppy.addFile(tagFile)
+        } catch (err) {
+          // Nothing, restriction errors handled in Core
+        }
       })
       .then(() => {
         const dashboard = this.uppy.getPlugin('Dashboard')

+ 3 - 2
src/plugins/Webcam/RecordButton.js

@@ -1,6 +1,7 @@
 const { h } = require('preact')
 
 module.exports = function RecordButton ({ recording, onStartRecording, onStopRecording, i18n }) {
+  console.log('is recording', recording)
   if (recording) {
     return (
       <button class="uppy-u-reset uppy-c-btn uppy-Webcam-button uppy-Webcam-button--video"
@@ -9,7 +10,7 @@ module.exports = function RecordButton ({ recording, onStartRecording, onStopRec
         aria-label={i18n('stopRecording')}
         onclick={onStopRecording}>
         <svg aria-hidden="true" class="UppyIcon" width="100" height="100" viewBox="0 0 100 100">
-          <circle cx="50" cy="50" r="40" />
+          <rect x="15" y="15" width="70" height="70" />
         </svg>
       </button>
     )
@@ -22,7 +23,7 @@ module.exports = function RecordButton ({ recording, onStartRecording, onStopRec
       aria-label={i18n('startRecording')}
       onclick={onStartRecording}>
       <svg aria-hidden="true" class="UppyIcon" width="100" height="100" viewBox="0 0 100 100">
-        <rect x="15" y="15" width="70" height="70" />
+        <circle cx="50" cy="50" r="40" />
       </svg>
     </button>
   )

+ 12 - 2
src/plugins/Webcam/index.js

@@ -173,7 +173,13 @@ module.exports = class Webcam extends Plugin {
       })
       return this.getVideo()
     })
-    .then((file) => this.uppy.addFile(file))
+    .then((file) => {
+      try {
+        this.uppy.addFile(file)
+      } catch (err) {
+        // Nothing, restriction errors handled in Core
+      }
+    })
     .then(() => {
       this.recordingChunks = null
       this.recorder = null
@@ -238,7 +244,11 @@ module.exports = class Webcam extends Plugin {
       this.captureInProgress = false
       const dashboard = this.uppy.getPlugin('Dashboard')
       if (dashboard) dashboard.hideAllPanels()
-      return this.uppy.addFile(tagFile)
+      try {
+        this.uppy.addFile(tagFile)
+      } catch (err) {
+        // Nothing, restriction errors handled in Core
+      }
     }, (error) => {
       this.captureInProgress = false
       throw error

+ 34 - 8
src/plugins/XHRUpload.js

@@ -102,15 +102,16 @@ module.exports = class XHRUpload extends Plugin {
   }
 
   getOptions (file) {
+    const overrides = this.uppy.getState().xhrUpload
     const opts = Object.assign({},
       this.opts,
-      this.uppy.state.xhrUpload || {},
+      overrides || {},
       file.xhrUpload || {}
     )
     opts.headers = {}
     Object.assign(opts.headers, this.opts.headers)
-    if (this.uppy.state.xhrUpload) {
-      Object.assign(opts.headers, this.uppy.state.xhrUpload.headers)
+    if (overrides) {
+      Object.assign(opts.headers, overrides.headers)
     }
     if (file.xhrUpload) {
       Object.assign(opts.headers, file.xhrUpload.headers)
@@ -126,6 +127,8 @@ module.exports = class XHRUpload extends Plugin {
   createProgressTimeout (timeout, timeoutHandler) {
     const uppy = this.uppy
     const self = this
+    let isDone = false
+
     function onTimedOut () {
       uppy.log(`[XHRUpload] timed out`)
       const error = new Error(self.i18n('timedOut', { seconds: Math.ceil(timeout / 1000) }))
@@ -134,17 +137,24 @@ module.exports = class XHRUpload extends Plugin {
 
     let aliveTimer = null
     function progress () {
+      // Some browsers fire another progress event when the upload is
+      // cancelled, so we have to ignore progress after the timer was
+      // told to stop.
+      if (isDone) return
+
       if (timeout > 0) {
-        done()
+        if (aliveTimer) clearTimeout(aliveTimer)
         aliveTimer = setTimeout(onTimedOut, timeout)
       }
     }
 
     function done () {
+      uppy.log(`[XHRUpload] timer done`)
       if (aliveTimer) {
         clearTimeout(aliveTimer)
         aliveTimer = null
       }
+      isDone = true
     }
 
     return {
@@ -281,8 +291,7 @@ module.exports = class XHRUpload extends Plugin {
       })
 
       this.uppy.on('cancel-all', () => {
-        // const files = this.uppy.getState().files
-        // if (!files[file.id]) return
+        timer.done()
         xhr.abort()
       })
     })
@@ -387,8 +396,8 @@ module.exports = class XHRUpload extends Plugin {
         files.forEach((file) => {
           this.uppy.emit('upload-progress', file, {
             uploader: this,
-            bytesUploaded: ev.loaded,
-            bytesTotal: ev.total
+            bytesUploaded: ev.loaded / ev.total * file.size,
+            bytesTotal: file.size
           })
         })
       })
@@ -419,6 +428,7 @@ module.exports = class XHRUpload extends Plugin {
       })
 
       this.uppy.on('cancel-all', () => {
+        timer.done()
         xhr.abort()
       })
 
@@ -479,10 +489,26 @@ module.exports = class XHRUpload extends Plugin {
   }
 
   install () {
+    if (this.opts.bundle) {
+      this.uppy.setState({
+        capabilities: Object.assign({}, this.uppy.getState().capabilities, {
+          bundled: true
+        })
+      })
+    }
+
     this.uppy.addUploader(this.handleUpload)
   }
 
   uninstall () {
+    if (this.opts.bundle) {
+      this.uppy.setState({
+        capabilities: Object.assign({}, this.uppy.getState().capabilities, {
+          bundled: true
+        })
+      })
+    }
+
     this.uppy.removeUploader(this.handleUpload)
   }
 }

+ 1 - 1
src/react/Dashboard.js

@@ -20,7 +20,7 @@ class Dashboard extends React.Component {
     delete options.uppy
     uppy.use(DashboardPlugin, options)
 
-    this.plugin = uppy.getPlugin('Dashboard')
+    this.plugin = uppy.getPlugin(options.id)
   }
 
   componentWillUnmount () {

+ 35 - 0
src/react/Dashboard.test.js

@@ -0,0 +1,35 @@
+const h = require('react').createElement
+const { mount, configure } = require('enzyme')
+const ReactAdapter = require('enzyme-adapter-react-16')
+const Uppy = require('../core')
+
+beforeAll(() => {
+  configure({ adapter: new ReactAdapter() })
+})
+
+jest.mock('../plugins/Dashboard', () => require('./__mocks__/DashboardPlugin'))
+
+const Dashboard = require('./Dashboard')
+
+describe('react <Dashboard />', () => {
+  it('can be mounted and unmounted', () => {
+    const oninstall = jest.fn()
+    const onuninstall = jest.fn()
+    const uppy = Uppy()
+    const dash = mount((
+      <Dashboard
+        uppy={uppy}
+        onInstall={oninstall}
+        onUninstall={onuninstall}
+      />
+    ))
+
+    expect(oninstall).toHaveBeenCalled()
+    expect(onuninstall).not.toHaveBeenCalled()
+
+    dash.unmount()
+
+    expect(oninstall).toHaveBeenCalled()
+    expect(onuninstall).toHaveBeenCalled()
+  })
+})

+ 1 - 1
src/react/DashboardModal.js

@@ -28,7 +28,7 @@ class DashboardModal extends React.Component {
     delete options.uppy
     uppy.use(DashboardPlugin, options)
 
-    this.plugin = uppy.getPlugin('Dashboard')
+    this.plugin = uppy.getPlugin(options.id)
     if (this.props.open) {
       this.plugin.openModal()
     }

+ 81 - 0
src/react/DashboardModal.test.js

@@ -0,0 +1,81 @@
+const h = require('react').createElement
+const { mount, configure } = require('enzyme')
+const ReactAdapter = require('enzyme-adapter-react-16')
+const Uppy = require('../core')
+
+jest.mock('../plugins/Dashboard', () => require('./__mocks__/DashboardPlugin'))
+
+const DashboardModal = require('./DashboardModal')
+
+beforeAll(() => {
+  configure({ adapter: new ReactAdapter() })
+})
+
+beforeEach(() => {
+  Object.assign(require('../plugins/Dashboard').prototype, {
+    openModal: jest.fn(),
+    closeModal: jest.fn()
+  })
+})
+
+describe('react <DashboardModal />', () => {
+  it('can be mounted and unmounted', () => {
+    const oninstall = jest.fn()
+    const onuninstall = jest.fn()
+    const uppy = Uppy()
+    const dash = mount((
+      <DashboardModal
+        uppy={uppy}
+        onInstall={oninstall}
+        onUninstall={onuninstall}
+      />
+    ))
+
+    expect(oninstall).toHaveBeenCalled()
+    expect(onuninstall).not.toHaveBeenCalled()
+
+    dash.unmount()
+
+    expect(oninstall).toHaveBeenCalled()
+    expect(onuninstall).toHaveBeenCalled()
+  })
+
+  it('opens the modal using the `open={true}` prop', () => {
+    const uppy = Uppy()
+    const dash = mount((
+      <DashboardModal
+        uppy={uppy}
+        open={false}
+      />
+    ))
+    const { plugin } = dash.instance()
+
+    expect(plugin.openModal).not.toHaveBeenCalled()
+
+    dash.setProps({ open: true })
+
+    expect(plugin.openModal).toHaveBeenCalled()
+
+    dash.unmount()
+  })
+
+  it('closes the modal using the `open={false}` prop', () => {
+    const uppy = Uppy()
+    const dash = mount((
+      <DashboardModal
+        uppy={uppy}
+        open
+      />
+    ))
+    const { plugin } = dash.instance()
+
+    expect(plugin.openModal).toHaveBeenCalled()
+    expect(plugin.closeModal).not.toHaveBeenCalled()
+
+    dash.setProps({ open: false })
+
+    expect(plugin.closeModal).toHaveBeenCalled()
+
+    dash.unmount()
+  })
+})

+ 1 - 1
src/react/DragDrop.js

@@ -21,7 +21,7 @@ class DragDrop extends React.Component {
 
     uppy.use(DragDropPlugin, options)
 
-    this.plugin = uppy.getPlugin('DragDrop')
+    this.plugin = uppy.getPlugin(options.id)
   }
 
   componentWillUnmount () {

+ 35 - 0
src/react/DragDrop.test.js

@@ -0,0 +1,35 @@
+const h = require('react').createElement
+const { mount, configure } = require('enzyme')
+const ReactAdapter = require('enzyme-adapter-react-16')
+const Uppy = require('../core')
+
+beforeAll(() => {
+  configure({ adapter: new ReactAdapter() })
+})
+
+jest.mock('../plugins/DragDrop', () => require('./__mocks__/DragDropPlugin'))
+
+const DragDrop = require('./DragDrop')
+
+describe('react <DragDrop />', () => {
+  it('can be mounted and unmounted', () => {
+    const oninstall = jest.fn()
+    const onuninstall = jest.fn()
+    const uppy = Uppy()
+    const dash = mount((
+      <DragDrop
+        uppy={uppy}
+        onInstall={oninstall}
+        onUninstall={onuninstall}
+      />
+    ))
+
+    expect(oninstall).toHaveBeenCalled()
+    expect(onuninstall).not.toHaveBeenCalled()
+
+    dash.unmount()
+
+    expect(oninstall).toHaveBeenCalled()
+    expect(onuninstall).toHaveBeenCalled()
+  })
+})

+ 1 - 1
src/react/ProgressBar.js

@@ -21,7 +21,7 @@ class ProgressBar extends React.Component {
 
     uppy.use(ProgressBarPlugin, options)
 
-    this.plugin = uppy.getPlugin('ProgressBar')
+    this.plugin = uppy.getPlugin(options.id)
   }
 
   componentWillUnmount () {

+ 35 - 0
src/react/ProgressBar.test.js

@@ -0,0 +1,35 @@
+const h = require('react').createElement
+const { mount, configure } = require('enzyme')
+const ReactAdapter = require('enzyme-adapter-react-16')
+const Uppy = require('../core')
+
+beforeAll(() => {
+  configure({ adapter: new ReactAdapter() })
+})
+
+jest.mock('../plugins/ProgressBar', () => require('./__mocks__/ProgressBarPlugin'))
+
+const ProgressBar = require('./ProgressBar')
+
+describe('react <ProgressBar />', () => {
+  it('can be mounted and unmounted', () => {
+    const oninstall = jest.fn()
+    const onuninstall = jest.fn()
+    const uppy = Uppy()
+    const dash = mount((
+      <ProgressBar
+        uppy={uppy}
+        onInstall={oninstall}
+        onUninstall={onuninstall}
+      />
+    ))
+
+    expect(oninstall).toHaveBeenCalled()
+    expect(onuninstall).not.toHaveBeenCalled()
+
+    dash.unmount()
+
+    expect(oninstall).toHaveBeenCalled()
+    expect(onuninstall).toHaveBeenCalled()
+  })
+})

+ 1 - 1
src/react/StatusBar.js

@@ -22,7 +22,7 @@ class StatusBar extends React.Component {
 
     uppy.use(StatusBarPlugin, options)
 
-    this.plugin = uppy.getPlugin('StatusBar')
+    this.plugin = uppy.getPlugin(options.id)
   }
 
   componentWillUnmount () {

+ 35 - 0
src/react/StatusBar.test.js

@@ -0,0 +1,35 @@
+const h = require('react').createElement
+const { mount, configure } = require('enzyme')
+const ReactAdapter = require('enzyme-adapter-react-16')
+const Uppy = require('../core')
+
+beforeAll(() => {
+  configure({ adapter: new ReactAdapter() })
+})
+
+jest.mock('../plugins/StatusBar', () => require('./__mocks__/StatusBarPlugin'))
+
+const StatusBar = require('./StatusBar')
+
+describe('react <StatusBar />', () => {
+  it('can be mounted and unmounted', () => {
+    const oninstall = jest.fn()
+    const onuninstall = jest.fn()
+    const uppy = Uppy()
+    const dash = mount((
+      <StatusBar
+        uppy={uppy}
+        onInstall={oninstall}
+        onUninstall={onuninstall}
+      />
+    ))
+
+    expect(oninstall).toHaveBeenCalled()
+    expect(onuninstall).not.toHaveBeenCalled()
+
+    dash.unmount()
+
+    expect(oninstall).toHaveBeenCalled()
+    expect(onuninstall).toHaveBeenCalled()
+  })
+})

+ 18 - 0
src/react/__mocks__/DashboardPlugin.js

@@ -0,0 +1,18 @@
+const Plugin = require('../../core/Plugin')
+
+module.exports = class Dashboard extends Plugin {
+  constructor (uppy, opts) {
+    super(uppy, opts)
+
+    this.id = opts.id
+    this.type = 'orchestrator'
+  }
+
+  install () {
+    if (this.opts.onInstall) this.opts.onInstall()
+  }
+
+  uninstall () {
+    if (this.opts.onUninstall) this.opts.onUninstall()
+  }
+}

+ 18 - 0
src/react/__mocks__/DragDropPlugin.js

@@ -0,0 +1,18 @@
+const Plugin = require('../../core/Plugin')
+
+module.exports = class DragDrop extends Plugin {
+  constructor (uppy, opts) {
+    super(uppy, opts)
+
+    this.id = opts.id
+    this.type = 'acquirer'
+  }
+
+  install () {
+    if (this.opts.onInstall) this.opts.onInstall()
+  }
+
+  uninstall () {
+    if (this.opts.onUninstall) this.opts.onUninstall()
+  }
+}

+ 18 - 0
src/react/__mocks__/ProgressBarPlugin.js

@@ -0,0 +1,18 @@
+const Plugin = require('../../core/Plugin')
+
+module.exports = class ProgressBar extends Plugin {
+  constructor (uppy, opts) {
+    super(uppy, opts)
+
+    this.id = opts.id
+    this.type = 'progressindicator'
+  }
+
+  install () {
+    this.opts.onInstall()
+  }
+
+  uninstall () {
+    this.opts.onUninstall()
+  }
+}

+ 18 - 0
src/react/__mocks__/StatusBarPlugin.js

@@ -0,0 +1,18 @@
+const Plugin = require('../../core/Plugin')
+
+module.exports = class StatusBar extends Plugin {
+  constructor (uppy, opts) {
+    super(uppy, opts)
+
+    this.id = opts.id
+    this.type = 'progressindicator'
+  }
+
+  install () {
+    this.opts.onInstall()
+  }
+
+  uninstall () {
+    this.opts.onUninstall()
+  }
+}

+ 0 - 23
src/scss/_animation.scss

@@ -1,23 +0,0 @@
-// Animation
-
-@keyframes zoomOutLeft {
-  40% {
-    opacity: 1;
-    -webkit-transform: scale3d(.475, .475, .475) translate3d(42px, 0, 0);
-    transform: scale3d(.475, .475, .475) translate3d(42px, 0, 0);
-  }
-
-  to {
-    opacity: 0;
-    -webkit-transform: scale(.1) translate3d(-2000px, 0, 0);
-    transform: scale(.1) translate3d(-2000px, 0, 0);
-    -webkit-transform-origin: left center;
-    transform-origin: left center;
-  }
-}
-
-.UppyAnimation-zoomOutLeft {
-  animation-name: zoomOutLeft;
-  animation-duration: 1s;
-  animation-fill-mode: both;
-}

+ 61 - 61
src/scss/_dashboard.scss

@@ -1,17 +1,51 @@
 .uppy-Dashboard--modal {
   z-index: $zIndex-2;
-  // transition: transform 0.2s ease-in-out;
-  // transform: none;
-  // -webkit-overflow-scrolling: touch;
 }
 
-.uppy-Dashboard--modal[aria-hidden=true] {
-  display: none;
-  // transform: translateY(-50%);
-}
+  .uppy-Dashboard--modal[aria-hidden=true] {
+    display: none;
+  }
+
+  // Modal open/close animations
+
+  @keyframes uppy-Dashboard-fadeIn {
+    from { opacity: 0;  }
+    to { opacity: 1;  }
+  }
+
+  @keyframes uppy-Dashboard-fadeOut {
+    from { opacity: 1;  }
+    to { opacity: 0;  }
+  }
+
+  @keyframes uppy-Dashboard-slideDownAndFadeIn {
+    from { transform: translate3d(-50%, -70%, 0); opacity: 0; }
+    to { transform: translate3d(-50%, -50%, 0); opacity: 1; }
+  }
+
+  @keyframes uppy-Dashboard-slideUpFadeOut {
+    from { transform: translate3d(-50%, -50%, 0); opacity: 1; }
+    to { transform: translate3d(-50%, -70%, 0); opacity: 0; }
+  }
+
+  .uppy-Dashboard--modal.uppy-Dashboard--animateOpenClose > .uppy-Dashboard-inner {
+    animation: uppy-Dashboard-slideDownAndFadeIn 0.3s cubic-bezier(0, 0, .2, 1);
+  }
+
+  .uppy-Dashboard--modal.uppy-Dashboard--animateOpenClose > .uppy-Dashboard-overlay {
+    animation: uppy-Dashboard-fadeIn 0.3s cubic-bezier(0, 0, .2, 1);
+  }
+
+  .uppy-Dashboard--modal.uppy-Dashboard--animateOpenClose.uppy-Dashboard--isClosing > .uppy-Dashboard-inner {
+    animation: uppy-Dashboard-slideUpFadeOut 0.3s cubic-bezier(0, 0, .2, 1);
+  }
+
+  .uppy-Dashboard--modal.uppy-Dashboard--animateOpenClose.uppy-Dashboard--isClosing > .uppy-Dashboard-overlay {
+    animation: uppy-Dashboard-fadeOut 0.3s cubic-bezier(0, 0, .2, 1);
+  }
 
 // Added to body to prevent the page from scrolling when Modal is open
-.uppy-Dashboard-isOpen {
+.uppy-Dashboard-isFixed {
   overflow: hidden;
   height: 100vh;
 }
@@ -206,6 +240,7 @@
   border: 0;
   background-color: transparent;
   -webkit-appearance: none;
+  appearance: none;
   // outline: none;
   transition: all 0.3s;
   color: darken($color-gray, 25%);
@@ -222,7 +257,7 @@
   margin-top: 5px;
   margin-bottom: 0;
   font-weight: 500;
-  overflow-x: hidden;
+  overflow: hidden;
   text-overflow: ellipsis;
   white-space: nowrap;
 
@@ -289,48 +324,31 @@
     font-size: 16px;
     line-height: 50px;
     max-width: 300px;
-    // top: 15px;
   }
 }
 
-.uppy-DashboardContent-titleFile {
-  // text-decoration: underline;
-}
-
 .uppy-DashboardContent-back {
   @include reset-button;
-  // position: absolute;
-  // top: 0;
-  // left: 15px;
   font-size: 14px;
-  // line-height: 40px;
   font-weight: 500;
   cursor: pointer;
   color: $color-cornflower-blue;
 
   .uppy-Dashboard--wide & {
     font-size: 15px;
-    // line-height: 50px;
   }
 }
 
-// .uppy-DashboardContent-back .UppyIcon {
-//   position: relative;
-//   margin-right: 3px;
-// }
-
 .uppy-DashboardContent-panel {
   position: absolute;
   top: 0;
   bottom: 0;
   left: 0;
   right: 0;
-  transform: translateY(-105%);
+  transform: translate3d(0, -105%, 0);
   transition: transform 0.2s ease-in-out;
-  will-change: transform;
   background-color: darken($color-white, 4%);
   box-shadow: 0 0 10px 5px rgba($color-black, 0.15);
-  // padding: 15px;
   padding-top: 40px;
   overflow: hidden;
   z-index: $zIndex-4;
@@ -338,14 +356,10 @@
   .uppy-Dashboard--wide & {
     padding-top: 50px;
   }
-
-  // .uppy-Dashboard--modal & {
-  //   z-index: $zIndex-4;
-  // }
 }
 
 .uppy-DashboardContent-panel[aria-hidden=false] {
-  transform: none;
+  transform: translate3d(0, 0, 0);
 }
 
 // Progress bar placeholder
@@ -376,6 +390,7 @@
   background: none;
   background-color: rgba($color-gray, 0.7);
   -webkit-appearance: none;
+  appearance: none;
   border: 0;
   z-index: $zIndex-3;
   transition: background-color 0.5s;
@@ -429,29 +444,6 @@
   border-color: darken($color-white, 20%);
 }
 
-// .uppy-Dashboard-bgIcon {
-  // width: 100%;
-  // max-width: 460px;
-  // position: absolute;
-  // top: 50%;
-  // left: 50%;
-  // transform: translate(-50%, -50%);
-  // opacity: 0.7;
-  // transition: all 0.3s;
-  // padding: 0 20px;
-// }
-
-// .uppy-Dashboard-bgIcon .UppyIcon {
-//   width: 100%;
-//   height: 80px;
-//   fill: none;
-//   stroke: $color-asphalt-gray;
-
-//   .uppy-Dashboard--wide & {
-//     height: 110px;
-//   }
-// }
-
 .uppy-Dashboard-bgIcon {
   height: 100%;
   display: flex;
@@ -792,10 +784,10 @@
 
 .uppy-DashboardItem-progressIndicator {
   @include reset-button;
+  display: inline-block;
   width: 38px;
   height: 38px;
   opacity: 0.9;
-  cursor: pointer;
   transition: all .35s ease;
 
   .uppy-Dashboard--wide & {
@@ -804,6 +796,10 @@
   }
 }
 
+  button.uppy-DashboardItem-progressIndicator {
+    cursor: pointer;
+  }
+
   .uppy-DashboardItem.is-error .uppy-DashboardItem-progressIndicator {
     width: 18px;
     height: 18px;
@@ -1012,8 +1008,9 @@
 //
 
 .uppy-DashboardFileCard {
-  transform: translateY(0);
-  transition: all 0.25s ease-in-out;
+  transform: translate3d(0, 0, 0);
+  transition: transform 0.2s ease-in-out;
+  
   width: 100%;
   height: 100%;
   position: absolute;
@@ -1026,9 +1023,9 @@
   background-color: $color-white;
 }
 
-.uppy-DashboardFileCard[aria-hidden=true] {
-  transform: translateY(-105%);
-}
+  .uppy-DashboardFileCard[aria-hidden=true] {
+    transform: translate3d(0, -105%, 0);
+  }
 
 .uppy-DashboardFileCard-inner {
   display: flex;
@@ -1093,3 +1090,6 @@
   vertical-align: middle;
   width: 78%;
 }
+
+
+

+ 5 - 1
src/scss/_statusbar.scss

@@ -189,8 +189,12 @@
   top: -1px;
   left: 6px;
   font-size: 10px;
-  // margin-left: -1px;
   text-align: center;
+  cursor: help;
+
+  &:hover {
+    cursor: help;
+  }
 }
 
 .uppy-StatusBar-details:after {

+ 4 - 3
src/server/RequestClient.js

@@ -10,13 +10,14 @@ module.exports = class RequestClient {
   }
 
   get hostname () {
-    const uppyServer = this.uppy.state.uppyServer || {}
+    const { uppyServer } = this.uppy.getState()
     const host = this.opts.host
-    return uppyServer[host] || host
+    return uppyServer && uppyServer[host] ? uppyServer[host] : host
   }
 
   onReceiveResponse (response) {
-    const uppyServer = this.uppy.state.uppyServer || {}
+    const state = this.uppy.getState()
+    const uppyServer = state.uppyServer || {}
     const host = this.opts.host
     const headers = response.headers
     // Store the self-identified domain name for the uppy-server we just hit.

+ 3 - 1
src/views/ProviderView/AuthView.js

@@ -3,7 +3,9 @@ const { h, Component } = require('preact')
 
 class AuthBlock extends Component {
   componentDidMount () {
-    this.connectButton.focus()
+    setTimeout(() => {
+      this.connectButton.focus({ preventScroll: true })
+    }, 150)
   }
 
   render () {

+ 5 - 1
src/views/ProviderView/index.js

@@ -196,7 +196,11 @@ module.exports = class ProviderView {
       tagFile.preview = this.plugin.getItemThumbnailUrl(file)
     }
     this.plugin.uppy.log('Adding remote file')
-    this.plugin.uppy.addFile(tagFile)
+    try {
+      this.plugin.uppy.addFile(tagFile)
+    } catch (err) {
+      // Nothing, restriction errors handled in Core
+    }
   }
 
   removeFile (id) {

+ 581 - 0
types/index.d.ts

@@ -0,0 +1,581 @@
+// Type definitions for uppy 0.24
+// Project: https://uppy.io
+// Definitions by: My Self <https://github.com/me>
+// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
+
+// TypeScript Version: 2.3
+
+// export as namespace Uppy;
+
+declare module 'uppy' {
+	export const Core: Core;
+	export const Dashboard: plugins.Dashboard;
+	export const DragDrop: plugins.DragDrop;
+	export const XHRUpload: plugins.XHRUpload;
+	export const GoogleDrive: plugins.GoogleDrive;
+	export const Instagram: plugins.Instagram;
+	export const Webcam: plugins.Webcam;
+	export const Tus: plugins.Tus;
+	export const StatusBar: plugins.StatusBar;
+	export const Url: plugins.Url;
+	export const Dropbox: plugins.Dropbox;
+	export const AwsS3: plugins.AwsS3;
+	export const GoldenRetriever: plugins.GoldenRetriever;
+	export const ThumbnailGenerator: plugins.ThumbnailGenerator;
+	export const Transloadit: plugins.Transloadit;
+	export const Dummy: plugins.Dummy;
+	export const FileInput: plugins.FileInput;
+	export const Form: plugins.Form;
+	export const Informer: plugins.Informer;
+	export const MagicLog: plugins.MagicLog;
+	export const ProgressBar: plugins.ProgressBar;
+	export const ReduxDevTools: plugins.ReduxDevTools;
+}
+
+declare module 'uppy/lib/core' {
+	export = Core;
+}
+
+declare module 'uppy/lib/plugins/Dashboard' {
+	const Dashboard: plugins.Dashboard;
+	export = Dashboard;
+}
+
+declare module 'uppy/lib/plugins/DragDrop' {
+	const DragDrop: plugins.DragDrop;
+	export = DragDrop;
+}
+
+declare module 'uppy/lib/plugins/XHRUpload' {
+	const XHRUpload: plugins.XHRUpload;
+	export = XHRUpload;
+}
+
+declare module 'uppy/lib/plugins/GoogleDrive' {
+	const GoogleDrive: plugins.GoogleDrive;
+	export = GoogleDrive;
+}
+
+declare module 'uppy/lib/plugins/Instagram' {
+	const Instagram: plugins.Instagram;
+	export = Instagram;
+}
+
+declare module 'uppy/lib/plugins/Webcam' {
+	const Webcam: plugins.Webcam;
+	export = Webcam;
+}
+
+declare module 'uppy/lib/plugins/Tus' {
+	const Tus: plugins.Tus;
+	export = Tus;
+}
+
+declare module 'uppy/lib/plugins/StatusBar' {
+	const StatusBar: plugins.StatusBar;
+	export = StatusBar;
+}
+
+declare module 'uppy/lib/plugins/Url' {
+	const Url: plugins.Url;
+	export = Url;
+}
+
+declare module 'uppy/lib/plugins/Dropbox' {
+	const Dropbox: plugins.Dropbox;
+	export = Dropbox;
+}
+
+declare module 'uppy/lib/plugins/AwsS3' {
+	const AwsS3: plugins.AwsS3;
+	export = AwsS3;
+}
+
+declare module 'uppy/lib/plugins/GoldenRetriever' {
+	const GoldenRetriever: plugins.GoldenRetriever;
+	export = GoldenRetriever;
+}
+
+declare module 'uppy/lib/plugins/ThumbnailGenerator' {
+	const ThumbnailGenerator: plugins.ThumbnailGenerator;
+	export = ThumbnailGenerator;
+}
+
+declare module 'uppy/lib/plugins/Transloadit' {
+	const Transloadit: plugins.Transloadit;
+	export = Transloadit;
+}
+
+declare module 'uppy/lib/plugins/Dummy' {
+	const Dummy: plugins.Dummy;
+	export = Dummy;
+}
+
+declare module 'uppy/lib/plugins/FileInput' {
+	const FileInput: plugins.FileInput;
+	export = FileInput;
+}
+
+declare module 'uppy/lib/plugins/Form' {
+	const Form: plugins.Form;
+	export = Form;
+}
+
+declare module 'uppy/lib/plugins/Informer' {
+	const Informer: plugins.Informer;
+	export = Informer;
+}
+
+declare module 'uppy/lib/plugins/MagicLog' {
+	const MagicLog: plugins.MagicLog;
+	export = MagicLog;
+}
+
+declare module 'uppy/lib/plugins/ProgressBar' {
+	const ProgressBar: plugins.ProgressBar;
+	export = ProgressBar;
+}
+
+declare module 'uppy/lib/plugins/ReduxDevTools' {
+	const ReduxDevTools: plugins.ReduxDevTools;
+	export = ReduxDevTools;
+}
+
+declare namespace Store {
+	// todo
+	type State = any;
+
+	type EventRemover = () => void;
+
+	interface Store {
+		getState(): State;
+		setState(patch: State): void;
+		subscribe(listener: any): EventRemover;
+	}
+
+	// todo
+	type DefaultStore = Store;
+	// todo
+	type ReduxStore = Store;
+}
+
+declare interface Local {
+	strings: {
+		youCanOnlyUploadX: {
+			0: string;
+			1: string;
+		},
+		youHaveToAtLeastSelectX: {
+			0: string;
+			1: string;
+		},
+		exceedsSize: string;
+		youCanOnlyUploadFileTypes: string;
+		uppyServerError: string;
+		failedToUpload: string;
+		noInternetConnection: string;
+		connectedToInternet: string;
+	};
+}
+
+declare namespace plugins {
+	interface DashboardOptions extends core.CoreConfig {
+		onRequestCloseModal: () => any;
+		disablePageScrollWhenModalOpen: boolean;
+		closeModalOnClickOutside: boolean;
+		trigger: string | HTMLElement;
+		inline: boolean;
+		defaultTabIcon: string;
+		hideUploadButton: boolean;
+		width: string;
+		height: string;
+		note: string;
+		showLinkToFileUploadResult: boolean;
+		proudlyDisplayPoweredByUppy: boolean;
+		metaFields: string[];
+		plugins: core.Plugin[];
+		disableStatusBar: boolean;
+		showProgressDetails: boolean;
+		hideProgressAfterFinish: boolean;
+		disableInformer: boolean;
+		disableThumbnailGenerator: boolean;
+	}
+	class Dashboard extends core.Plugin {
+		constructor(uppy: core.Uppy, opts: Partial<DashboardOptions>);
+		addTarget(plugin: core.Plugin): HTMLElement;
+		hideAllPanels(): void;
+		showPanel(id: string): void;
+		requestCloseModal(): void;
+		getFocusableNodes(): HTMLElement[];
+		setFocusToFirstNode(): void;
+		setFocusToBrowse(): void;
+		maintainFocus(): void;
+		openModal(): void;
+		closeModal(): void;
+		isModalOpen(): boolean;
+		onKeydown(event: KeyboardEvent): void;
+		handleClickOutside(): void;
+		handlePaste(ev: ClipboardEvent): void;
+		handleInputChange(ev: Event): void;
+		initEvents(): void;
+		removeEvents(): void;
+		updateDashboardElWidth(): void;
+		toggleFileCard(fileId: string): void;
+		handleDrop(files: File[] | FileList): void;
+		render(state: Store.State): void;
+		discoverProviderPlugins(): void;
+		install(): void;
+		uninstall(): void;
+	}
+	interface DragDropOptions extends core.CoreConfig {
+		inputName: string;
+		allowMultipleFiles: boolean;
+		width: string;
+		height: string;
+		note: string;
+	}
+	class DragDrop extends core.Plugin {
+		constructor(uppy: core.Uppy, opts: Partial<DragDropOptions>);
+		checkDragDropSupport(): void;
+		handleDrop(files: File[] | FileList): void;
+		handleInputChange(ev: Event): void;
+	}
+	interface FileOptions {
+		headers: any;
+		[key: string]: any;
+	}
+	interface ProcessTimeout {
+		done(): void;
+		process(): void;
+	}
+	interface FormDataUploadOptions {
+		metaFields: string[];
+		fieldName: string;
+	}
+	interface XHRUploadOptions extends core.CoreConfig {
+		limit: string;
+		bundle: boolean;
+		formData: FormData;
+		headers: any;
+		metaFields: string[];
+		fieldName: string;
+		timeout: number;
+		responseUrlFieldName: string;
+		endpoint: string;
+		method: 'GET' | 'POST' | 'HEAD';
+	}
+	class XHRUpload extends core.Plugin {
+		constructor(uppy: core.Uppy, opts: Partial<XHRUploadOptions>);
+		getOptions(file: File): FileOptions;
+		createProgressTimeout(timeout: number, timeoutHandler: any): ProcessTimeout;
+		createFormDataUpload(file: File, opts: Partial<FormDataUploadOptions>): FormData;
+		createBareUpload(file: File): any;
+		upload(file: File, current: number, total: number): Promise<File>;
+		uploadRemote(file: File, current: number, total: number): Promise<File>;
+		uploadBundle(files: File[] | FileList): Promise<void>;
+		uploadBundle(files: File[] | FileList): Promise<{
+			successful: File[];
+			failed: any;
+		}>;
+		handleUpload(fileIDs: string[]): Promise<null>;
+	}
+	interface GoogleDriveOptions extends core.CoreConfig {
+		expires: number;
+		serviceWorker: boolean;
+		indexedDB: any;
+		host: string;
+	}
+	class GoogleDrive extends core.Plugin {
+		constructor(uppy: core.Uppy, opts: Partial<GoogleDriveOptions>);
+		loadFilesStateFromLocalStorage(): void;
+		getWaitingFiles(): { [fileID: string]: File };
+		getUploadingFiles(): { [fileID: string]: File };
+		saveFilesStateToLocalStorage(): void;
+		loadFileBlobsFromServiceWorker(): void;
+		loadFileBlobsFromIndexedDB(): void;
+		deleteBlobs(): Promise<any>;
+	}
+	interface InstagramOptions extends core.CoreConfig {
+		host: string;
+	}
+	class Instagram extends core.Plugin {
+		constructor(uppy: core.Uppy, opts: Partial<InstagramOptions>);
+	}
+	interface WebcamOptions extends core.CoreConfig {
+		countdown: boolean;
+	}
+	interface WebcamMedia {
+		source: string;
+		name: string;
+		data: File;
+		type: string;
+	}
+	class Webcam extends core.Plugin {
+		constructor(uppy: core.Uppy, opts: Partial<WebcamOptions>);
+		isSupported(): boolean;
+		getConstraints(): { audio: boolean; video: boolean; };
+		start(): Promise<void>;
+		startRecording(): void;
+		stopRecording(): Promise<void>;
+		stop(): void;
+		getVideoElement(): HTMLVideoElement;
+		oneTwoThreeSmile(): Promise<void>;
+		takeSnapshot(): void;
+		getImage(): Promise<WebcamMedia>;
+		getVideo(): Promise<WebcamMedia>;
+		focus(): void;
+	}
+	interface TusOptions extends core.CoreConfig {
+		limit: number;
+		endpoint: string;
+		uploadUrl: string;
+		useFastRemoteRetry: boolean;
+		resume: boolean;
+		autoRetry: boolean;
+	}
+	class Tus extends core.Plugin {
+		constructor(uppy: core.Uppy, opts: Partial<TusOptions>);
+	}
+	interface StatusBarOptions extends core.CoreConfig {
+		showProgressDetails: boolean;
+		hideUploadButton: boolean;
+		hideAfterFinish: boolean;
+	}
+	class StatusBar extends core.Plugin {
+		constructor(uppy: core.Uppy, opts: Partial<StatusBarOptions>);
+	}
+	interface UrlOptions extends core.CoreConfig {
+		host: string;
+	}
+	class Url extends core.Plugin {
+		constructor(uppy: core.Uppy, opts: Partial<UrlOptions>);
+	}
+	interface DropboxOptions extends core.CoreConfig {
+		host: string;
+	}
+	class Dropbox extends core.Plugin {
+		constructor(uppy: core.Uppy, opts: Partial<DropboxOptions>);
+	}
+	interface AwsS3Options extends core.CoreConfig {
+		limit: number;
+		host: string;
+		timeout: number;
+	}
+	class AwsS3 extends core.Plugin {
+		constructor(uppy: core.Uppy, opts: Partial<AwsS3Options>);
+	}
+	interface GoldenRetrieverOptions extends core.CoreConfig {
+		expires: number;
+		serviceWorker: boolean;
+		indexedDB: any;
+	}
+	class GoldenRetriever extends core.Plugin {
+		constructor(uppy: core.Uppy, opts: Partial<GoldenRetrieverOptions>);
+	}
+	interface ThumbnailGeneratorOptions extends core.CoreConfig {
+		thumbnailWidth: number;
+	}
+	class ThumbnailGenerator extends core.Plugin {
+		constructor(uppy: core.Uppy, opts: Partial<ThumbnailGeneratorOptions>);
+	}
+	interface TransloaditOptions extends core.CoreConfig {
+		params: any;
+		service: string;
+		waitForEncoding: boolean;
+		waitForMetadata: boolean;
+		importFromUploadURLs: boolean;
+		alwaysRunAssembly: boolean;
+	}
+	class Transloadit extends core.Plugin {
+		constructor(uppy: core.Uppy, opts: Partial<TransloaditOptions>);
+	}
+	interface DummyOptions extends core.CoreConfig {
+	}
+	class Dummy extends core.Plugin {
+		constructor(uppy: core.Uppy, opts: Partial<DummyOptions>);
+	}
+	interface FileInputOptions extends core.CoreConfig {
+		pretty: boolean;
+		inputName: string;
+	}
+	class FileInput extends core.Plugin {
+		constructor(uppy: core.Uppy, opts: Partial<FileInputOptions>);
+	}
+	interface FormOptions extends core.CoreConfig {
+		getMetaFromForm: boolean;
+		addResultToForm: boolean;
+		submitOnSuccess: boolean;
+		triggerUploadOnSubmit: boolean;
+		resultName: string;
+	}
+	class Form extends core.Plugin {
+		constructor(uppy: core.Uppy, opts: Partial<FormOptions>);
+	}
+	interface TypeColor {
+		[type: string]: {
+			bg: string | number;
+			text: string | number;
+		};
+	}
+	interface InformerOptions extends core.CoreConfig {
+		typeColors: TypeColor;
+	}
+	class Informer extends core.Plugin {
+		constructor(uppy: core.Uppy, opts: Partial<InformerOptions>);
+	}
+	interface MagicLogOptions extends core.CoreConfig {
+	}
+	class MagicLog extends core.Plugin {
+		constructor(uppy: core.Uppy, opts: Partial<MagicLogOptions>);
+	}
+	interface ProgressBarOptions extends core.CoreConfig {
+		hideAfterFinish: boolean;
+		fixed: boolean;
+	}
+	class ProgressBar extends core.Plugin {
+		constructor(uppy: core.Uppy, opts: Partial<ProgressBarOptions>);
+	}
+	interface ReduxDevToolsOptions extends core.CoreConfig {
+	}
+	class ReduxDevTools extends core.Plugin {
+		constructor(uppy: core.Uppy, opts: Partial<ReduxDevToolsOptions>);
+	}
+}
+
+declare namespace core {
+	interface CoreConfig {
+		id: string;
+		autoProceed: boolean;
+		debug: boolean;
+		restrictions: {
+			maxFileSize: false,
+			maxNumberOfFiles: false,
+			minNumberOfFiles: false,
+			allowedFileTypes: false
+		};
+		target: string | Plugin;
+		meta: any;
+		// onBeforeFileAdded: (currentFile, files) => currentFile,
+		// onBeforeUpload: (files) => files,
+		locale: Local;
+		store: Store.Store;
+	}
+	class Plugin {
+		constructor(uppy: Uppy, opts?: {
+			replaceTargetContent?: boolean;
+		});
+		getPluginState(): Store.State;
+		setPluginState(update: any): Store.State;
+		update(state?: Store.State): void;
+		mount(target: any, plugin: any): void;
+		render(state: Store.State): void;
+		addTarget(plugin: any): void;
+		unmount(): void;
+		install(): void;
+		uninstall(): void;
+	}
+	type LogType = 'info' | 'warning' | 'error';
+	interface SuccessedFile {
+		data: File;
+		extension: string;
+		id: string;
+		isPaused: boolean;
+		isRemote: boolean;
+		meta: {
+			name: string;
+			type: string;
+		};
+		name: string;
+		preview: string;
+		progress: {
+			uploadStarted: number;
+			uploadComplete: boolean;
+			percentage: number;
+			bytesUploaded: number;
+			bytesTotal: number;
+		};
+		remote: string;
+		size: number;
+		source: string;
+		type: string;
+		uploadURL: string;
+	}
+	interface Result {
+		failed: any[];
+		successful: SuccessedFile[];
+	}
+	class Uppy {
+		constructor(conf: Partial<CoreConfig>);
+		on(event: string, callback: (...args: any[]) => any): Uppy;
+		on(event: 'upload-success', callback: (fileCount: File, body: any, uploadurl: string) => any): Uppy;
+		on(event: 'complete', callback: (result: Result) => void): Uppy;
+		off(event: string, callback: any): Uppy;
+		updateAll(state: Store.State): void;
+		setState(patch: Store.State): void;
+		getState(): Store.State;
+		readonly state: Store.State;
+		setFileState(fileID: string, state: Store.State): void;
+		resetProgress(): void;
+		addPreProcessor(fn: any): void;
+		removePreProcessor(fn: any): void;
+		addPostProcessor(fn: any): void;
+		removePostProcessor(fn: any): void;
+		addUploader(fn: any): void;
+		removeUploader(fn: any): void;
+		setMeta(data: any): void;
+		setFileMeta(fileID: string, data: any): void;
+		getFile(fileID: string): File;
+		getFiles(): File[];
+		addFile(file: File): void;
+		removeFile(fileID: string): void;
+		pauseResume(fileID: string): boolean;
+		pauseAll(): void;
+		resumeAll(): void;
+		retryAll(): void;
+		cancelAll(): void;
+		retryUpload(fileID: string): any;
+		reset(): void;
+		actions(): void;
+		updateOnlineStatus(): void;
+		getID(): string;
+		// use<T extends Plugin>(Plugin: T, opts: any): Uppy;
+		use(Plugin: plugins.Dashboard, opts: Partial<plugins.DashboardOptions>): Uppy;
+		use(Plugin: plugins.DragDrop, opts: Partial<plugins.DragDropOptions>): Uppy;
+		use(Plugin: plugins.XHRUpload, opts: Partial<plugins.XHRUploadOptions>): Uppy;
+		use(Plugin: plugins.GoogleDrive, opts: Partial<plugins.GoogleDriveOptions>): Uppy;
+		use(Plugin: plugins.Instagram, opts: Partial<plugins.InstagramOptions>): Uppy;
+		use(Plugin: plugins.Webcam, opts: Partial<plugins.WebcamOptions>): Uppy;
+		use(Plugin: plugins.Tus, opts: Partial<plugins.TusOptions>): Uppy;
+		use(Plugin: plugins.StatusBar, opts: Partial<plugins.StatusBarOptions>): Uppy;
+		use(Plugin: plugins.Url, opts: Partial<plugins.UrlOptions>): Uppy;
+		use(Plugin: plugins.Dropbox, opts: Partial<plugins.DropboxOptions>): Uppy;
+		use(Plugin: plugins.AwsS3, opts: Partial<plugins.AwsS3Options>): Uppy;
+		use(Plugin: plugins.GoldenRetriever, opts: Partial<plugins.GoldenRetrieverOptions>): Uppy;
+		use(Plugin: plugins.ThumbnailGenerator, opts: Partial<plugins.ThumbnailGeneratorOptions>): Uppy;
+		use(Plugin: plugins.Transloadit, opts: Partial<plugins.TransloaditOptions>): Uppy;
+		use(Plugin: plugins.Dummy, opts: Partial<plugins.DummyOptions>): Uppy;
+		use(Plugin: plugins.FileInput, opts: Partial<plugins.FileInputOptions>): Uppy;
+		use(Plugin: plugins.Form, opts: Partial<plugins.FormOptions>): Uppy;
+		use(Plugin: plugins.Informer, opts: Partial<plugins.InformerOptions>): Uppy;
+		use(Plugin: plugins.MagicLog, opts: Partial<plugins.MagicLogOptions>): Uppy;
+		use(Plugin: plugins.ProgressBar, opts: Partial<plugins.ProgressBarOptions>): Uppy;
+		use(Plugin: plugins.ReduxDevTools, opts: Partial<plugins.ReduxDevToolsOptions>): Uppy;
+		getPlugin(name: string): Plugin;
+		iteratePlugins(method: any): void;
+		removePlugin(instance: Plugin): void;
+		close(): void;
+		info(message: string | { message: string; details: string; }, type?: LogType, duration?: number): void;
+		hideInfo(): void;
+		log(msg: string, type?: LogType): void;
+		run(): Uppy;
+		restore(uploadID: string): Promise<any>;
+		addResultData(uploadID: string, data: any): void;
+		upload(): Promise<any>;
+	}
+}
+
+interface Core {
+	(conf?: Partial<core.CoreConfig>): core.Uppy;
+}
+
+declare function Core(conf?: Partial<core.CoreConfig>): core.Uppy;

+ 28 - 0
types/tsconfig.json

@@ -0,0 +1,28 @@
+{
+	"compilerOptions": {
+		"target": "esnext",
+		"module": "commonjs",
+		"lib": [
+				"dom",
+				"esnext"
+		],
+		"noImplicitAny": true,
+		"noImplicitThis": true,
+		"strictNullChecks": true,
+		"baseUrl": "../",
+		"typeRoots": [
+		"../"
+		],
+		"types": [],
+		"noEmit": true,
+		"esModuleInterop": true,
+		"allowSyntheticDefaultImports": true,
+		"strictFunctionTypes": true,
+		"forceConsistentCasingInFileNames": true
+	},
+	"files": [
+	"index.d.ts",
+	"uppy-tests.ts",
+	"uppy-tests-official.ts"
+	]
+}

+ 1 - 0
types/tslint.json

@@ -0,0 +1 @@
+{ "extends": "dtslint/dt.json" }

+ 7 - 0
types/uppy-tests-official.ts

@@ -0,0 +1,7 @@
+import Core from 'uppy/lib/core';
+import Dashboard from 'uppy/lib/plugins/Dashboard';
+
+(() => {
+	const uppy = Core();
+	uppy.use(Dashboard, {});
+})();

+ 36 - 0
types/uppy-tests.ts

@@ -0,0 +1,36 @@
+import Uppy = require('uppy');
+
+(() => {
+	const uppy = Uppy.Core({ autoProceed: false });
+	uppy.use(Uppy.Dashboard, { trigger: '#up_load_file_01' });
+	uppy.use(Uppy.DragDrop, { target: '#ttt' });
+	uppy.use(Uppy.XHRUpload, {
+		bundle: true,
+		endpoint: 'xxx',
+		fieldName: 'up_load_file'
+	});
+	uppy.run();
+	uppy.on('upload-success', (fileCount, body, uploadurl) => {
+		console.log(fileCount, body, uploadurl, ` files uploaded`);
+	});
+})();
+
+(() => {
+	const uppy = Uppy.Core({ autoProceed: false })
+		.use(Uppy.Dashboard, { trigger: '#select-files' })
+		.use(Uppy.GoogleDrive, { target: Uppy.Dashboard, host: 'https://server.uppy.io' })
+		.use(Uppy.Instagram, { target: Uppy.Dashboard, host: 'https://server.uppy.io' })
+		.use(Uppy.Webcam, { target: Uppy.Dashboard })
+		.use(Uppy.Tus, { endpoint: 'https://master.tus.io/files/' })
+		.run()
+		.on('complete', (result) => {
+			console.log('Upload result:', result);
+		});
+})();
+
+(() => {
+	const uppy = Uppy.Core();
+	uppy.use(Uppy.DragDrop, { target: '.UppyDragDrop' });
+	uppy.use(Uppy.Tus, { endpoint: '//master.tus.io/files/' });
+	uppy.run();
+})();

+ 13 - 1
website/src/docs/dashboard.md

@@ -99,7 +99,15 @@ By default, progress in StatusBar is shown as simple percentage. If you’d like
 
 ### `hideUploadButton: false`
 
-Hide the upload button. Use this if you are providing a custom upload button somewhere on the page using the `uppy.upload()` API.
+Hide the upload button. Use this if you are providing a custom upload button somewhere, and using the `uppy.upload()` API.
+
+## `hideRetryButton: false`
+
+Hide the retry button. Use this if you are providing a custom retry button somewhere, and using the `uppy.retryAll()` or `uppy.retryUpload(fileID)` API.
+
+## `hidePauseResumeCancelButtons: false`
+
+Hide cancel or pause/resume (for resumable uploads, via [tus](http://tus.io), for example) buttons. Use this if you are providing a custom cancel or pause/resume buttons somewhere, and using the `uppy.pauseResume(fileID)`, `uppy.cancelAll()` or `uppy.removeFile(fileID)` API.
 
 ### `hideProgressAfterFinish: false`
 
@@ -138,6 +146,10 @@ Set to true to automatically close the modal when the user clicks outside of it.
 
 By default when Dashboard modal is open, it will disable page scrolling, so when you scroll a list of files in Uppy the website in the background stays still. Set to false to override this behaviour and leave page scrolling intact.
 
+## `animateOpenClose: true`
+
+Add light animations when modal dialog is open or closed, for more satisfying user experience.
+
 ### `proudlyDisplayPoweredByUppy: true`
 
 Uppy is provided for the world for free by the [Transloadit team](https://transloadit.com). In return, we ask that you consider keeping a tiny Uppy logo at the bottom of the Dashboard, so that more people can discover and use Uppy.

+ 10 - 0
website/src/docs/uppy.md

@@ -244,6 +244,16 @@ uppy.addFile({
 
 If `uppy.opts.autoProceed === true`, Uppy will begin uploading automatically when files are added.
 
+### `uppy.removeFile(fileID)`
+
+Remove a file from Uppy.
+
+```js
+uppy.removeFile('uppyteamkongjpg1501851828779')
+```
+
+Removing a file that is already being uploaded cancels that upload.
+
 ### `uppy.getFile(fileID)`
 
 Get a specific file object by its ID.