Forráskód Böngészése

Merge branch 'master' into hide-buttons-status-bar

Artur Paikin 7 éve
szülő
commit
2b06af0fc9
99 módosított fájl, 2509 hozzáadás és 908 törlés
  1. 29 13
      CHANGELOG.md
  2. 7 9
      README.md
  3. 1 1
      bin/upload-to-cdn.sh
  4. 0 2
      examples/aws-presigned-url/main.js
  5. 0 2
      examples/aws-uppy-server/main.js
  6. 0 1
      examples/bundled-example/main.js
  7. 2 3
      examples/cdn-example/index.html
  8. 0 1
      examples/custom-provider/client/main.js
  9. 0 2
      examples/digitalocean-spaces/main.js
  10. 0 2
      examples/multiple-instances/main.js
  11. 0 2
      examples/react-example/App.js
  12. 0 1
      examples/redux/main.js
  13. 2 3
      examples/uppy-with-server/client/index.html
  14. 0 2
      examples/xhr-bundle/main.js
  15. 240 240
      package-lock.json
  16. 1 1
      package.json
  17. 27 28
      src/core/Core.js
  18. 27 26
      src/core/Core.test.js
  19. 28 4
      src/core/Plugin.js
  20. 33 4
      src/core/Translator.js
  21. 19 3
      src/core/Translator.test.js
  22. 0 3
      src/core/UppySocket.js
  23. 6 117
      src/core/Utils.js
  24. 12 2
      src/core/Utils.test.js
  25. 36 0
      src/core/mime-types.js
  26. 424 0
      src/plugins/AwsS3/Multipart.js
  27. 284 0
      src/plugins/AwsS3/MultipartUploader.js
  28. 13 8
      src/plugins/Dashboard/ActionBrowseTagline.js
  29. 6 6
      src/plugins/Dashboard/Dashboard.js
  30. 16 10
      src/plugins/Dashboard/FileCard.js
  31. 1 1
      src/plugins/Dashboard/FileItem.js
  32. 29 26
      src/plugins/Dashboard/FileList.js
  33. 8 6
      src/plugins/Dashboard/Tabs.js
  34. 13 8
      src/plugins/Dashboard/index.js
  35. 23 7
      src/plugins/DragDrop/index.js
  36. 6 6
      src/plugins/Dropbox/icons.js
  37. 4 0
      src/plugins/Dropbox/index.js
  38. 5 2
      src/plugins/FileInput.js
  39. 12 0
      src/plugins/GoogleDrive/index.js
  40. 4 0
      src/plugins/Instagram/index.js
  41. 1 0
      src/plugins/StatusBar/index.js
  42. 16 14
      src/plugins/Transloadit/index.js
  43. 0 1
      src/plugins/Transloadit/index.test.js
  44. 42 23
      src/plugins/Tus.js
  45. 62 0
      src/plugins/Url/index.js
  46. 12 6
      src/scss/_dashboard.scss
  47. 2 12
      src/scss/_dragdrop.scss
  48. 9 2
      src/scss/_provider.scss
  49. 15 0
      src/server/RequestClient.js
  50. 27 14
      src/views/ProviderView/AuthView.js
  51. 3 1
      src/views/ProviderView/Browser.js
  52. 2 2
      src/views/ProviderView/Item.js
  53. 3 11
      src/views/ProviderView/ItemList.js
  54. 4 1
      src/views/ProviderView/index.js
  55. 1 5
      test/endtoend/src/main.js
  56. 1 2
      website/src/api-usage-example.ejs
  57. 119 0
      website/src/docs/aws-s3-multipart.md
  58. 25 1
      website/src/docs/aws-s3.md
  59. 76 47
      website/src/docs/dashboard.md
  60. 30 11
      website/src/docs/dragdrop.md
  61. 53 0
      website/src/docs/dropbox.md
  62. 21 9
      website/src/docs/fileinput.md
  63. 13 1
      website/src/docs/form.md
  64. 0 1
      website/src/docs/golden-retriever.md
  65. 53 0
      website/src/docs/google-drive.md
  66. 2 4
      website/src/docs/index.md
  67. 16 0
      website/src/docs/informer.md
  68. 53 0
      website/src/docs/instagram.md
  69. 6 49
      website/src/docs/plugins.md
  70. 16 0
      website/src/docs/progressbar.md
  71. 37 0
      website/src/docs/providers.md
  72. 79 0
      website/src/docs/react-dashboard-modal.md
  73. 16 51
      website/src/docs/react-dashboard.md
  74. 26 0
      website/src/docs/react-dragdrop.md
  75. 25 0
      website/src/docs/react-progressbar.md
  76. 26 0
      website/src/docs/react-statusbar.md
  77. 11 9
      website/src/docs/react.md
  78. 4 4
      website/src/docs/redux.md
  79. 71 30
      website/src/docs/statusbar.md
  80. 48 1
      website/src/docs/transloadit.md
  81. 10 0
      website/src/docs/tus.md
  82. 19 13
      website/src/docs/uppy.md
  83. 62 0
      website/src/docs/url.md
  84. 32 6
      website/src/docs/webcam.md
  85. 1 1
      website/src/docs/writing-plugins.md
  86. 26 4
      website/src/docs/xhrupload.md
  87. 0 1
      website/src/examples/bundle/app.html
  88. 0 1
      website/src/examples/dashboard/app.es6
  89. 0 1
      website/src/examples/dashboard/index.ejs
  90. 0 2
      website/src/examples/dragdrop/app.es6
  91. 2 3
      website/src/examples/i18n/app.html
  92. 0 1
      website/src/examples/statusbar/app.es6
  93. 0 1
      website/src/examples/transloadit/app.es6
  94. 1 2
      website/src/examples/transloadit/index.ejs
  95. 0 1
      website/src/examples/xhrupload/app.es6
  96. 0 1
      website/src/uppy.ejs
  97. 4 5
      website/themes/uppy/layout/index.ejs
  98. 7 0
      website/themes/uppy/layout/partials/sidebar.ejs
  99. 1 1
      website/themes/uppy/layout/partials/social.ejs

+ 29 - 13
CHANGELOG.md

@@ -32,7 +32,7 @@ PRs are welcome! Please do open an issue to discuss first if it's a big feature,
 - [ ] maybe restrict system file picking dialog too https://github.com/transloadit/uppy/issues/253
 - [ ] uppy-server: what happens if access token expires amid an upload/download process.
 - [ ] good way to change plugin options at runtime—maybe `this.state.options`?
-- [ ] s3: multipart/"resumable" uploads for large files (@goto-bus-stop)
+- [x] s3: multipart/"resumable" uploads for large files (@goto-bus-stop)
 - [ ] DnD Bar: drag and drop + statusbar or progressbar ? (@arturi)
 - [ ] possibility to work on already uploaded / in progress files #112, #113
 - [ ] possibility to edit/delete more than one file at once #118, #97
@@ -66,6 +66,8 @@ Sort of like jQuery UI: https://jqueryui.com/download/
 - [ ] core: css-in-js, while keeping non-random classnames (ideally prefixed) and useful preprocessor features. also see simple https://github.com/codemirror/CodeMirror/blob/master/lib/codemirror.css (@arturi, @goto-bus-stop)
 - [ ] webcam: Stop recording when file size is exceeded, should be possible given how the MediaRecorder API works
 - [ ] dashboard: add option to disable uploading from local disk #657
+- [ ] dashboard: display data like image resolution on file cards #783
+- [ ] server: pass metadata to S3 `getKey` option, see https://github.com/transloadit/uppy/issues/689
 
 ## 1.0 Goals
 
@@ -73,14 +75,15 @@ What we need to do to release Uppy 1.0
 
 - [ ] QA: test how everything works together: user experience from `npm install` to production build with Webpack, using in React/Redux environment (npm pack)
 - [ ] QA: test in multiple browsers and mobile devices again
-- [ ] QA: test uppy server. benchmarks / stress test. multiple connections, different setups, large files (10 GB)
+- [x] QA: test uppy server. benchmarks / stress test. multiple connections, different setups, large files (10 GB)
 - [ ] QA: tests for some plugins
 - [x] docs: on using plugins, all options, list of plugins, i18n
 - [ ] feature: preset for Transloadit that mimics jQuery SDK, check https://github.com/transloadit/jquery-sdk docs
-- [ ] refactoring: possibly add CSS-in-JS
+- [ ] refactoring: possibly add CSS-in-JS, style encapsulation
 - [x] refactoring: possibly switch from Yo-Yo to Preact, because it’s more stable, solves a few issues we are struggling with (onload being weird/hard/modern-browsers-only with bel; no way to pass refs to elements; extra network requests with base64 urls) and mature, “new standard”, larger community
 - [ ] refactoring: split uppy into small packages, lerna repo?
 - [x] QA: tests for core and utils
+- [ ] feature: basic Reacte Native support
 - [x] feature: Redux and ReduxDevTools support (currently mirrors Uppy state to Redux)
 - [x] feature: beta file recovering after closed tab / browser crash
 - [x] feature: easy integration with React (UppyReact components)
@@ -92,13 +95,16 @@ What we need to do to release Uppy 1.0
 - [x] uppy-server: security audit
 - [x] uppy-server: storing tokens in user’s browser only (d040281cc9a63060e2f2685c16de0091aee5c7b4)
 
+# 0.26.0
+
+- [ ] dashboard: allow minimizing the Dashboard during upload (Uppy then becomes just a tiny progress indicator) (@arturi)
+
 # next
 
 ## 0.25.0
 
 To Be Released: 2018-05-31.
 
-- [ ] dashboard: allow minimizing the Dashboard during upload (Uppy then becomes just a tiny progress indicator) (@arturi)
 - [ ] dashboard: cancel button for transloadit assemblies (@arturi, @goto-bus-stop)
 - [ ] core: figure out per-plugin locales and i18n strings packs #491
 - [ ] goldenretriever: confirmation before restore #443
@@ -115,16 +121,26 @@ To Be Released: 2018-05-31.
 - [ ] xhrupload: emit a final `upload-progress` event in the XHRUpload plugin just before firing `upload-complete` (tus-js-client already handles this internally) (@arturi)
 - [ ] core: add more mime-to-extension mappings from https://github.com/micnic/mime.json/blob/master/index.json (which ones?) # (@arturi, @goto-bus-stop)
 - [ ] 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)
-- [ ] tus: add `filename` and `filetype`, so that tus servers knows what headers to set  https://github.com/tus/tus-js-client/commit/ebc5189eac35956c9f975ead26de90c896dbe360
+- [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
-- [ ] core: Pass allowedFileTypes and maxNumberOfFiles to input[type=file] (@arturi / #814)
-- [x] statusbar: add some spacing between text elements (#760 / @goto-bus-stop)
+- [x] core: removed .run() (to solve issues like #756), update ddocs (#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)
-- [ ] docs: improve on React docs https://uppy.io/docs/react/, add small example for each component maybe? Dashboard, DragDrop, ProgressBar? No need to make separate pages for all of them, just headings on the same page. Right now docs are confusing, because they focus on DashboardModal. Also problems with syntax highlight on https://uppy.io/docs/react/dashboard-modal/.
-- [ ] docs: add note in docs or solve the .run() issue, see #756
-
+- [ ] docs: improve on React docs https://uppy.io/docs/react/, add small example for each component maybe? Dashboard, DragDrop, ProgressBar? No need to make separate pages for all of them, just headings on the same page. Right now docs are confusing, because they focus on DashboardModal. Also problems with syntax highlight on https://uppy.io/docs/react/dashboard-modal/ (@goto-bus-stop)
 - [ ] core: customizing metadata fields, boolean metadata; see #809, #454 and related (@arturi)
+- [x] providers: Add user/account names to Uppy provider views (61bf0a7 / @ifedapoolarewaju)
+- [x] s3: implement multipart uploads (#726 / @goto-bus-stop)
+
+## 0.24.4
+
+Released: 2018-05-14.
+
+- core: Pass `allowedFileTypes` and `maxNumberOfFiles` to input[type=file] in UI components: Dashboard, DragDrop, FileInput (#814 / @arturi)
+- transloadit: Update Transloadit plugin's Uppy Server handling (#804 / @goto-bus-stop)
+- tus: respect `limit` option for upload parameter requests (#817 / @ap--)
+- docs: Explain name `metadata` vs. `$_FILES[]["name"]` (#1c1bf2e / @goto-bus-stop)
+- dashboard: improve “powered by” icon (#0284c8e / @arturi)
+- statusbar: add default string for cancel button (#822 / @mrbatista)
 
 ## 0.24.3
 
@@ -135,15 +151,15 @@ Released: 2018-05-10.
 - dashboard: fix duplicate plugin IDs, see #702 (@goto-bus-stop)
 - dashboard/statusbar: fix some unicode characters showing up as gibberish (#787 / @goto-bus-stop)
 - dashboard: Fix grid item height in remote providers with few files (#791 / @goto-bus-stop)
-- dashboard: Add `rel="noopener noreferrer"` to links containing `target="_blank" (#767 / @kvz)
+- dashboard: Add `rel="noopener noreferrer"` to links containing `target="_blank"` (#767 / @kvz)
 - instagram: add extensions to instagram files (@ifedapoolarewaju)
-- transloadit: More robust failure handling for Transloadit, closes #708 
-(#805 / @goto-bus-stop)
+- transloadit: More robust failure handling for Transloadit, closes #708 (#805 / @goto-bus-stop)
 - docs: Document "headers" upload parameter in AwsS3 plugin (#780 / @janko-m)
 - docs: Update some `uppy.state` docs to align with the Stores feature (#792 / @goto-bus-stop)
 - dragdrop: Add `inputName` option like FileInput has, set empty value="", closes #729 (#778 / @goto-bus-stop, @arturi)
 - docs: Google Cloud Storage setup for the AwsS3 plugin (#777 / goto-bus-stop)
 - react: Update React component PropTypes (#776 / @arturi)
+- statusbar: add some spacing between text elements (#760 / @goto-bus-stop)
 
 ## 0.24.2
 

+ 7 - 9
README.md

@@ -35,7 +35,6 @@ const uppy = Uppy({ autoProceed: false })
   .use(Instagram, { target: Dashboard, host: 'https://server.uppy.io' })
   .use(Webcam, { target: Dashboard })
   .use(Tus, { endpoint: 'https://master.tus.io/files/' })
-  .run()
   .on('complete', (result) => {
     console.log('Upload result:', result)
   })
@@ -64,7 +63,7 @@ $ npm install uppy --save
 
 We recommend installing from npm and then using a module bundler such as [Webpack](http://webpack.github.io/), [Browserify](http://browserify.org/) or [Rollup.js](http://rollupjs.org/).
 
-Add CSS [uppy.min.css](https://transloadit.edgly.net/releases/uppy/v0.24.3/dist/uppy.min.css), either to `<head>` of your HTML page or include in JS, if your bundler of choice supports it — transforms and plugins are available for Browserify and Webpack.
+Add CSS [uppy.min.css](https://transloadit.edgly.net/releases/uppy/v0.24.4/dist/uppy.min.css), either to `<head>` of your HTML page or include in JS, if your bundler of choice supports it — transforms and plugins are available for Browserify and Webpack.
 
 Alternatively, you can also use a pre-built bundle from Transloadit's CDN: Edgly. In that case `Uppy` will attach itself to the global `window.Uppy` object.
 
@@ -73,12 +72,12 @@ Alternatively, you can also use a pre-built bundle from Transloadit's CDN: Edgly
 1\. Add a script to the bottom of `<body>`:
 
 ``` html
-<script src="https://transloadit.edgly.net/releases/uppy/v0.24.3/dist/uppy.min.js"></script>
+<script src="https://transloadit.edgly.net/releases/uppy/v0.24.4/dist/uppy.min.js"></script>
 ```
 
 2\. Add CSS to `<head>`:
 ``` html
-<link href="https://transloadit.edgly.net/releases/uppy/v0.24.3/dist/uppy.min.css" rel="stylesheet">
+<link href="https://transloadit.edgly.net/releases/uppy/v0.24.4/dist/uppy.min.css" rel="stylesheet">
 ```
 
 3\. Initialize:
@@ -89,7 +88,6 @@ Alternatively, you can also use a pre-built bundle from Transloadit's CDN: Edgly
   var uppy = Uppy.Core()
   uppy.use(Uppy.DragDrop, { target: '.UppyDragDrop' })
   uppy.use(Uppy.Tus, { endpoint: '//master.tus.io/files/' })
-  uppy.run()
 </script>
 ```
 
@@ -135,8 +133,8 @@ We aim to support IE10+ and recent versions of Safari, Edge, Chrome, Firefox, an
 Having no JavaScript beats having a lot of it, so that’s a fair question! Running an uploading & encoding business for ten years though we found that in cases, the file input leaves some to be desired:
 
 - We received complaints about broken uploads and found that resumable uploads are important, especially for big files and to be inclusive towards people on poorer connections (we also launched [tus.io](https://tus.io) to attack that problem). Uppy uploads can survive network outages and browser crashes or accidental navigate-aways.
-- Uppy supports editing meta information before uploading (and e.g. cropping is planned). 
-- There’s the situation where people are using their mobile devices and want to upload on the go, but they have their picture on Instagram, files in Dropbox, or just a plain file url from anywhere on the open web. Uppy allows to pick files from those and push it to the destination without downloading it to your mobile device first. 
+- Uppy supports editing meta information before uploading (and e.g. cropping is planned).
+- There’s the situation where people are using their mobile devices and want to upload on the go, but they have their picture on Instagram, files in Dropbox, or just a plain file url from anywhere on the open web. Uppy allows to pick files from those and push it to the destination without downloading it to your mobile device first.
 - Accurate upload progress reporting is an issue on many platforms.
 - Some file validation — size, type, number of files — can be done on the client with Uppy.
 - Uppy integrates webcam support, in case your users want to upload a picture/video/audio that does not exist yet :)
@@ -148,7 +146,7 @@ Not all apps need all of these features. A `<input type="file">` is fine in many
 
 ### Why is all this goodness free?
 
-Transloadit’s team is small and we have a shared ambition to make a living from open source. By giving away projects like [tus.io](https://tus.io) and [Uppy](https://uppy.io),we’re hoping to advance the state of the art, make life a tiny little bit better for everyone, and in doing so have rewarding jobs and get some eyes on our commercial service: [a content ingestion & processing platform](https://transloadit.com). 
+Transloadit’s team is small and we have a shared ambition to make a living from open source. By giving away projects like [tus.io](https://tus.io) and [Uppy](https://uppy.io),we’re hoping to advance the state of the art, make life a tiny little bit better for everyone, and in doing so have rewarding jobs and get some eyes on our commercial service: [a content ingestion & processing platform](https://transloadit.com).
 
 Our thinking is that if just a fraction of our open source userbase can see the appeal of hosted versions straight from the source, that could already be enough to sustain our work. So far this is working out! We’re able to dedicate 80% of our time to open source and haven’t gone bankrupt just yet :D
 
@@ -162,7 +160,7 @@ Yes, there is an S3 plugin, please check out the [docs](https://uppy.io/docs/aws
 
 ### Do I need to install special service/server for Uppy? Can I use it with Rails/Node/Go/PHP?
 
-Yes, whatever you want on the backend will work with `XHRUpload` plugin, since it just does a `POST` or `PUT` request. Here’s a [PHP backend example](https://uppy.io/docs/xhrupload/#Uploading-to-a-PHP-Server). 
+Yes, whatever you want on the backend will work with `XHRUpload` plugin, since it just does a `POST` or `PUT` request. Here’s a [PHP backend example](https://uppy.io/docs/xhrupload/#Uploading-to-a-PHP-Server).
 
 If you want resumability with the Tus plugin, use [one of the tus server implementations](https://tus.io/implementations.html) 👌🏼
 

+ 1 - 1
bin/upload-to-cdn.sh

@@ -8,7 +8,7 @@
 #  - Checks if a tag is being built (on Travis - otherwise opts to continue execution regardless)
 #  - Installs AWS CLI if needed
 #  - Assumed a fully built uppy is in root dir (unless a specific tag was specified, then it's fetched from npm)
-#  - Runs npm pack, and stores files to e.g. https://transloadit.edgly.net/releases/uppy/v0.24.3/dist/uppy.css
+#  - Runs npm pack, and stores files to e.g. https://transloadit.edgly.net/releases/uppy/v0.24.4/dist/uppy.css
 #  - Uses local package by default, if [version] argument was specified, takes package from npm
 #
 # Run as:

+ 0 - 2
examples/aws-presigned-url/main.js

@@ -38,5 +38,3 @@ uppy.use(AwsS3, {
     })
   }
 })
-
-uppy.run()

+ 0 - 2
examples/aws-uppy-server/main.js

@@ -21,5 +21,3 @@ uppy.use(Dashboard, {
 uppy.use(AwsS3, {
   host: 'http://localhost:3020'
 })
-
-uppy.run()

+ 0 - 1
examples/bundled-example/main.js

@@ -34,7 +34,6 @@ const uppy = Uppy({
   .use(Tus, { endpoint: TUS_ENDPOINT })
   .use(Form, { target: '#upload-form' })
   // .use(GoldenRetriever, {serviceWorker: true})
-  .run()
 
 uppy.on('complete', (result) => {
   if (result.failed.length === 0) {

+ 2 - 3
examples/cdn-example/index.html

@@ -4,17 +4,16 @@
     <title></title>
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1">
-    <link href="https://transloadit.edgly.net/releases/uppy/v0.24.3/dist/uppy.min.css" rel="stylesheet">
+    <link href="https://transloadit.edgly.net/releases/uppy/v0.24.4/dist/uppy.min.css" rel="stylesheet">
   </head>
   <body>
     <button id="uppyModalOpener">Open Modal</button>
-    <script src="https://transloadit.edgly.net/releases/uppy/v0.24.3/dist/uppy.min.js"></script>
+    <script src="https://transloadit.edgly.net/releases/uppy/v0.24.4/dist/uppy.min.js"></script>
     <script>
       const uppy = Uppy.Core({debug: true, autoProceed: false})
         .use(Uppy.Dashboard, { trigger: '#uppyModalOpener' })
         .use(Uppy.Webcam, {target: Uppy.Dashboard})
         .use(Uppy.Tus, { endpoint: 'https://master.tus.io/files/' })
-        .run()
 
       uppy.on('success', (fileCount) => {
         console.log(`${fileCount} files uploaded`)

+ 0 - 1
examples/custom-provider/client/main.js

@@ -24,4 +24,3 @@ uppy.use(Dashboard, {
 })
 
 uppy.use(Tus, {endpoint: 'https://master.tus.io/files/'})
-uppy.run()

+ 0 - 2
examples/digitalocean-spaces/main.js

@@ -14,5 +14,3 @@ uppy.use(Dashboard, {
 
 // No client side changes needed!
 uppy.use(AwsS3, { host: '/uppy-server' })
-
-uppy.run()

+ 0 - 2
examples/multiple-instances/main.js

@@ -14,7 +14,6 @@ const a = Uppy({
     width: 400
   })
   .use(GoldenRetriever, { serviceWorker: false })
-  .run()
 
 const b = Uppy({
   id: 'b',
@@ -26,7 +25,6 @@ const b = Uppy({
     width: 400
   })
   .use(GoldenRetriever, { serviceWorker: false })
-  .run()
 
 window.a = a
 window.b = b

+ 0 - 2
examples/react-example/App.js

@@ -21,11 +21,9 @@ module.exports = class App extends React.Component {
     this.uppy = new Uppy({ autoProceed: false })
       .use(Tus, { endpoint: 'https://master.tus.io/files/' })
       .use(GoogleDrive, { host: 'https://server.uppy.io' })
-      .run()
 
     this.uppy2 = new Uppy({ autoProceed: false })
       .use(Tus, { endpoint: 'https://master.tus.io/files/' })
-      .run()
   }
 
   componentWillUnmount () {

+ 0 - 1
examples/redux/main.js

@@ -79,6 +79,5 @@ uppy.use(Dashboard, {
   width: 400
 })
 uppy.use(Tus, { endpoint: 'https://master.tus.io/' })
-uppy.run()
 
 window.uppy = uppy

+ 2 - 3
examples/uppy-with-server/client/index.html

@@ -4,18 +4,17 @@
     <title></title>
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1">
-    <link href="https://transloadit.edgly.net/releases/uppy/v0.24.3/dist/uppy.min.css" rel="stylesheet">
+    <link href="https://transloadit.edgly.net/releases/uppy/v0.24.4/dist/uppy.min.css" rel="stylesheet">
   </head>
   <body>
     <button id="uppyModalOpener">Open Modal</button>
-    <script src="https://transloadit.edgly.net/releases/uppy/v0.24.3/dist/uppy.min.js"></script>
+    <script src="https://transloadit.edgly.net/releases/uppy/v0.24.4/dist/uppy.min.js"></script>
     <script>
       const uppy = Uppy.Core({debug: true, autoProceed: false})
         .use(Uppy.Dashboard, { trigger: '#uppyModalOpener' })
         .use(Uppy.Instagram, { target: Uppy.Dashboard, host: 'http://localhost:3020' })
         .use(Uppy.GoogleDrive, { target: Uppy.Dashboard, host: 'http://localhost:3020' })
         .use(Uppy.Tus, { endpoint: 'https://master.tus.io/files/' })
-        .run()
 
       uppy.on('success', (fileCount) => {
         console.log(`${fileCount} files uploaded`)

+ 0 - 2
examples/xhr-bundle/main.js

@@ -19,5 +19,3 @@ uppy.use(XHRUpload, {
   endpoint: 'http://localhost:9967/upload',
   fieldName: 'files'
 })
-
-uppy.run()

A különbségek nem kerülnek megjelenítésre, a fájl túl nagy
+ 240 - 240
package-lock.json


+ 1 - 1
package.json

@@ -1,6 +1,6 @@
 {
   "name": "uppy",
-  "version": "0.24.3",
+  "version": "0.24.4",
   "description": "Extensible JavaScript file upload widget with support for drag&drop, resumable uploads, previews, restrictions, file processing/encoding, remote providers like Instagram, Dropbox, Google Drive, S3 and more :dog:",
   "main": "lib/index.js",
   "jsnext:main": "src/index.js",

+ 27 - 28
src/core/Core.js

@@ -31,9 +31,10 @@ class Uppy {
         exceedsSize: 'This file exceeds maximum allowed size of',
         youCanOnlyUploadFileTypes: 'You can only upload:',
         uppyServerError: 'Connection with Uppy Server failed',
-        failedToUpload: 'Failed to upload',
+        failedToUpload: 'Failed to upload %{file}',
         noInternetConnection: 'No Internet connection',
-        connectedToInternet: 'Connected to the Internet'
+        connectedToInternet: 'Connected to the Internet',
+        noFilesFound: 'You have no files or folders here'
       }
     }
 
@@ -43,10 +44,10 @@ class Uppy {
       autoProceed: true,
       debug: false,
       restrictions: {
-        maxFileSize: false,
-        maxNumberOfFiles: false,
-        minNumberOfFiles: false,
-        allowedFileTypes: false
+        maxFileSize: null,
+        maxNumberOfFiles: null,
+        minNumberOfFiles: null,
+        allowedFileTypes: null
       },
       meta: {},
       onBeforeFileAdded: (currentFile, files) => currentFile,
@@ -128,6 +129,8 @@ class Uppy {
       window['uppyLog'] = ''
       window[this.opts.id] = this
     }
+
+    this._addListeners()
   }
 
   on (event, callback) {
@@ -322,8 +325,20 @@ class Uppy {
 
     if (allowedFileTypes) {
       const isCorrectFileType = allowedFileTypes.filter((type) => {
-        if (!file.type) return false
-        return match(file.type, type)
+        // if (!file.type) return false
+
+        // is this is a mime-type
+        if (type.indexOf('/') > -1) {
+          if (!file.type) return false
+          return match(file.type, type)
+        }
+
+        // otherwise this is likely an extension
+        if (type[0] === '.') {
+          if (file.extension === type.substr(1)) {
+            return file.extension
+          }
+        }
       }).length > 0
 
       if (!isCorrectFileType) {
@@ -632,20 +647,8 @@ class Uppy {
   /**
    * Registers listeners for all global actions, like:
    * `error`, `file-removed`, `upload-progress`
-   *
    */
-  actions () {
-    // const log = this.log
-    // this.on('*', function (payload) {
-    //   log(`[Core] Event: ${this.event}`)
-    //   log(payload)
-    // })
-
-    // stress-test re-rendering
-    // setInterval(() => {
-    //   this.setState({bla: 'bla'})
-    // }, 20)
-
+  _addListeners () {
     this.on('error', (error) => {
       this.setState({ error: error.message })
     })
@@ -654,8 +657,7 @@ class Uppy {
       this.setFileState(file.id, { error: error.message })
       this.setState({ error: error.message })
 
-      let message
-      message = `${this.i18n('failedToUpload')} ${file.name}`
+      let message = this.i18n('failedToUpload', { file: file.name })
       if (typeof error === 'object' && error.message) {
         message = { message: message, details: error.message }
       }
@@ -973,13 +975,10 @@ class Uppy {
   }
 
   /**
-   * Initializes actions.
-   *
+   * Obsolete, event listeners are now added in the constructor.
    */
   run () {
-    this.log('Core is run, initializing actions...')
-    this.actions()
-
+    this.log('Calling run() is no longer necessary.', 'warning')
     return this
   }
 

+ 27 - 26
src/core/Core.test.js

@@ -21,11 +21,6 @@ describe('src/Core', () => {
     jest.spyOn(utils, 'findDOMElement').mockImplementation(path => {
       return 'some config...'
     })
-    jest.spyOn(utils, 'createThumbnail').mockImplementation(path => {
-      return Promise.resolve(`data:image/jpeg;base64,${sampleImage.toString('base64')}`)
-    })
-    utils.createThumbnail.mockClear()
-
     global.URL.createObjectURL = jest.fn().mockReturnValue('newUrl')
   })
 
@@ -298,7 +293,6 @@ describe('src/Core', () => {
       core.addUploader((fileIDs, uploadID) => {
         core.addResultData(uploadID, { upload: 'ok' })
       })
-      core.run()
       return core.upload().then((result) => {
         expect(result.pre).toBe('ok')
         expect(result.upload).toBe('ok')
@@ -357,7 +351,6 @@ describe('src/Core', () => {
 
     it('should update the file progress state when preprocess-progress event is fired', () => {
       const core = new Core()
-      core.run()
       core.addFile({
         source: 'jest',
         name: 'foo.jpg',
@@ -384,7 +377,6 @@ describe('src/Core', () => {
 
     it('should update the file progress state when preprocess-complete event is fired', () => {
       const core = new Core()
-      core.run()
 
       core.addFile({
         source: 'jest',
@@ -465,7 +457,6 @@ describe('src/Core', () => {
 
     it('should update the file progress state when postprocess-progress event is fired', () => {
       const core = new Core()
-      core.run()
 
       core.addFile({
         source: 'jest',
@@ -493,7 +484,6 @@ describe('src/Core', () => {
 
     it('should update the file progress state when postprocess-complete event is fired', () => {
       const core = new Core()
-      core.run()
 
       core.addFile({
         source: 'jest',
@@ -564,7 +554,6 @@ describe('src/Core', () => {
       const fileData = new File([sampleImage], { type: 'image/jpeg' })
       const fileAddedEventMock = jest.fn()
       const core = new Core()
-      core.run()
       core.on('file-added', fileAddedEventMock)
 
       core.addFile({
@@ -638,7 +627,7 @@ describe('src/Core', () => {
 
   describe('uploading a file', () => {
     it('should return a { successful, failed } pair containing file objects', () => {
-      const core = new Core().run()
+      const core = new Core()
       core.addUploader((fileIDs) => Promise.resolve())
 
       core.addFile({ source: 'jest', name: 'foo.jpg', type: 'image/jpeg', data: new Uint8Array() })
@@ -654,7 +643,7 @@ describe('src/Core', () => {
     })
 
     it('should return files with errors in the { failed } key', () => {
-      const core = new Core().run()
+      const core = new Core()
       core.addUploader((fileIDs) => {
         fileIDs.forEach((fileID) => {
           const file = core.getFile(fileID)
@@ -679,7 +668,7 @@ describe('src/Core', () => {
     })
 
     it('should only upload files that are not already assigned to another upload id', () => {
-      const core = new Core().run()
+      const core = new Core()
       core.store.state.currentUploads = {
         upload1: {
           fileIDs: ['uppy-file1jpg-image/jpeg', 'uppy-file2jpg-image/jpeg', 'uppy-file3jpg-image/jpeg']
@@ -738,7 +727,6 @@ describe('src/Core', () => {
 
       const core = new Core()
       core.on('file-removed', fileRemovedEventMock)
-      core.run()
 
       core.addFile({
         source: 'jest',
@@ -952,7 +940,6 @@ describe('src/Core', () => {
     it('should reset the progress', () => {
       const resetProgressEvent = jest.fn()
       const core = new Core()
-      core.run()
       core.on('reset-progress', resetProgressEvent)
 
       core.addFile({
@@ -1042,7 +1029,7 @@ describe('src/Core', () => {
 
     xit('should enforce the minNumberOfFiles rule', () => {})
 
-    it('should enfore the allowedFileTypes rule', () => {
+    it('should enforce the allowedFileTypes rule', () => {
       const core = new Core({
         autoProceed: false,
         restrictions: {
@@ -1064,6 +1051,28 @@ describe('src/Core', () => {
       }
     })
 
+    it('should enforce the allowedFileTypes rule with file extensions', () => {
+      const core = new Core({
+        autoProceed: false,
+        restrictions: {
+          allowedFileTypes: ['.gif', '.jpg', '.jpeg']
+        }
+      })
+
+      try {
+        core.addFile({
+          source: 'jest',
+          name: 'foo2.png',
+          type: '',
+          data: new File([sampleImage], { type: 'image/jpeg' })
+        })
+        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')
+      }
+    })
+
     it('should enforce the maxFileSize rule', () => {
       const core = new Core({
         autoProceed: false,
@@ -1090,14 +1099,12 @@ describe('src/Core', () => {
   describe('actions', () => {
     it('should update the state when receiving the error event', () => {
       const core = new Core()
-      core.run()
       core.emit('error', new Error('foooooo'))
       expect(core.state.error).toEqual('foooooo')
     })
 
     it('should update the state when receiving the upload-error event', () => {
       const core = new Core()
-      core.run()
       core.state.files['fileId'] = {
         name: 'filename'
       }
@@ -1107,7 +1114,6 @@ describe('src/Core', () => {
 
     it('should reset the error state when receiving the upload event', () => {
       const core = new Core()
-      core.run()
       core.emit('error', { foo: 'bar' })
       core.emit('upload')
       expect(core.state.error).toEqual(null)
@@ -1165,7 +1171,6 @@ describe('src/Core', () => {
     it('should set a string based message to be displayed infinitely', () => {
       const infoVisibleEvent = jest.fn()
       const core = new Core()
-      core.run()
       core.on('info-visible', infoVisibleEvent)
 
       core.info('This is the message', 'info', 0)
@@ -1182,7 +1187,6 @@ describe('src/Core', () => {
     it('should set a object based message to be displayed infinitely', () => {
       const infoVisibleEvent = jest.fn()
       const core = new Core()
-      core.run()
       core.on('info-visible', infoVisibleEvent)
 
       core.info({
@@ -1207,7 +1211,6 @@ describe('src/Core', () => {
       const infoVisibleEvent = jest.fn()
       const infoHiddenEvent = jest.fn()
       const core = new Core()
-      core.run()
       core.on('info-visible', infoVisibleEvent)
       core.on('info-hidden', infoHiddenEvent)
 
@@ -1230,7 +1233,6 @@ describe('src/Core', () => {
       const infoVisibleEvent = jest.fn()
       const infoHiddenEvent = jest.fn()
       const core = new Core()
-      core.run()
       core.on('info-visible', infoVisibleEvent)
       core.on('info-hidden', infoHiddenEvent)
 
@@ -1251,7 +1253,6 @@ describe('src/Core', () => {
   describe('createUpload', () => {
     it('should assign the specified files to a new upload', () => {
       const core = new Core()
-      core.run()
       core.addFile({
         source: 'jest',
         name: 'foo.jpg',
@@ -1295,7 +1296,7 @@ describe('src/Core', () => {
       })
 
       expect(core.opts.restrictions.maxNumberOfFiles).toBe(3)
-      expect(core.opts.restrictions.minNumberOfFiles).toBe(false)
+      expect(core.opts.restrictions.minNumberOfFiles).toBe(null)
     })
   })
 })

+ 28 - 4
src/core/Plugin.js

@@ -1,6 +1,28 @@
 const preact = require('preact')
 const { findDOMElement } = require('../core/Utils')
 
+/**
+ * Defer a frequent call to the microtask queue.
+ */
+function debounce (fn) {
+  let calling = null
+  let latestArgs = null
+  return (...args) => {
+    latestArgs = args
+    if (!calling) {
+      calling = Promise.resolve().then(() => {
+        calling = null
+        // At this point `args` may be different from the most
+        // recent state, if multiple calls happened since this task
+        // was queued. So we use the `latestArgs`, which definitely
+        // is the most recent call.
+        return fn(...latestArgs)
+      })
+    }
+    return calling
+  }
+}
+
 /**
  * Boilerplate that all Plugins share - and should not be used
  * directly. It also shows which methods final plugins should implement/override,
@@ -39,8 +61,8 @@ module.exports = class Plugin {
       return
     }
 
-    if (this.updateUI) {
-      this.updateUI(state)
+    if (this._updateUI) {
+      this._updateUI(state)
     }
   }
 
@@ -60,9 +82,11 @@ module.exports = class Plugin {
     if (targetElement) {
       this.isTargetDOMEl = true
 
-      this.updateUI = (state) => {
+      // API for plugins that require a synchronous rerender.
+      this.rerender = (state) => {
         this.el = preact.render(this.render(state), targetElement, this.el)
       }
+      this._updateUI = debounce(this.rerender)
 
       this.uppy.log(`Installing ${callerPluginName} to a DOM element`)
 
@@ -71,7 +95,7 @@ module.exports = class Plugin {
         targetElement.innerHTML = ''
       }
 
-      this.el = preact.render(this.render(this.uppy.state), targetElement)
+      this.el = preact.render(this.render(this.uppy.getState()), targetElement)
 
       return this.el
     }

+ 33 - 4
src/core/Translator.js

@@ -41,9 +41,10 @@ module.exports = class Translator {
    * @return {string} interpolated
    */
   interpolate (phrase, options) {
-    const replace = String.prototype.replace
+    const { split, replace } = String.prototype
     const dollarRegex = /\$/g
     const dollarBillsYall = '$$$$'
+    let interpolated = [phrase]
 
     for (let arg in options) {
       if (arg !== '_' && options.hasOwnProperty(arg)) {
@@ -57,10 +58,28 @@ module.exports = class Translator {
         // We create a new `RegExp` each time instead of using a more-efficient
         // string replace so that the same argument can be replaced multiple times
         // in the same phrase.
-        phrase = replace.call(phrase, new RegExp('%\\{' + arg + '\\}', 'g'), replacement)
+        interpolated = insertReplacement(interpolated, new RegExp('%\\{' + arg + '\\}', 'g'), replacement)
       }
     }
-    return phrase
+
+    return interpolated
+
+    function insertReplacement (source, rx, replacement) {
+      const newParts = []
+      source.forEach((chunk) => {
+        split.call(chunk, rx).forEach((raw, i, list) => {
+          if (raw !== '') {
+            newParts.push(raw)
+          }
+
+          // Interlace with the `replacement` value
+          if (i < list.length - 1) {
+            newParts.push(replacement)
+          }
+        })
+      })
+      return newParts
+    }
   }
 
   /**
@@ -71,7 +90,17 @@ module.exports = class Translator {
    * @return {string} translated (and interpolated)
    */
   translate (key, options) {
-    if (options && options.smart_count) {
+    return this.translateArray(key, options).join('')
+  }
+
+  /**
+   * Get a translation and return the translated and interpolated parts as an array.
+   * @param {string} key
+   * @param {object} options with values that will be used to replace placeholders
+   * @return {Array} The translated and interpolated parts, in order.
+   */
+  translateArray (key, options) {
+    if (options && typeof options.smart_count !== 'undefined') {
       var plural = this.locale.pluralize(options.smart_count)
       return this.interpolate(this.opts.locale.strings[key][plural], options)
     }

+ 19 - 3
src/core/Translator.test.js

@@ -8,6 +8,22 @@ describe('core/translator', () => {
       const core = new Core({ locale: russian })
       expect(core.translator.translate('chooseFile')).toEqual('Выберите файл')
     })
+
+    it('should translate a string with non-string elements', () => {
+      const core = new Core({
+        locale: {
+          strings: {
+            test: 'Hello %{who}!',
+            test2: 'Hello %{who}'
+          }
+        }
+      })
+
+      const who = Symbol('who')
+      expect(core.translator.translateArray('test', { who: who })).toEqual(['Hello ', who, '!'])
+      // No empty string at the end.
+      expect(core.translator.translateArray('test2', { who: who })).toEqual(['Hello ', who])
+    })
   })
 
   describe('interpolation', () => {
@@ -23,15 +39,15 @@ describe('core/translator', () => {
     it('should translate a string', () => {
       const core = new Core({ locale: russian })
       expect(
-        core.translator.translate('filesChosen', { smart_count: '18' })
+        core.translator.translate('filesChosen', { smart_count: 18 })
       ).toEqual('Выбрано 18 файлов')
 
       expect(
-        core.translator.translate('filesChosen', { smart_count: '1' })
+        core.translator.translate('filesChosen', { smart_count: 1 })
       ).toEqual('Выбран 1 файл')
 
       expect(
-        core.translator.translate('filesChosen', { smart_count: '0' })
+        core.translator.translate('filesChosen', { smart_count: 0 })
       ).toEqual('Выбрано 0 файлов')
     })
   })

+ 0 - 3
src/core/UppySocket.js

@@ -51,12 +51,10 @@ module.exports = class UppySocket {
   }
 
   on (action, handler) {
-    console.log(action)
     this.emitter.on(action, handler)
   }
 
   emit (action, payload) {
-    console.log(action)
     this.emitter.emit(action, payload)
   }
 
@@ -67,7 +65,6 @@ module.exports = class UppySocket {
   _handleMessage (e) {
     try {
       const message = JSON.parse(e.data)
-      console.log(message)
       this.emit(message.action, message.payload)
     } catch (err) {
       console.log(err)

+ 6 - 117
src/core/Utils.js

@@ -1,4 +1,5 @@
 const throttle = require('lodash.throttle')
+const mimeTypes = require('./mime-types.js')
 
 /**
  * A collection of small utility functions that help with dom manipulation, adding listeners,
@@ -112,34 +113,21 @@ function getArrayBuffer (chunk) {
 }
 
 function getFileType (file) {
-  const extensionsToMime = {
-    'md': 'text/markdown',
-    'markdown': 'text/markdown',
-    'mp4': 'video/mp4',
-    'mp3': 'audio/mp3',
-    'svg': 'image/svg+xml',
-    'jpg': 'image/jpeg',
-    'png': 'image/png',
-    'gif': 'image/gif',
-    'yaml': 'text/yaml',
-    'yml': 'text/yaml'
-  }
-
   const fileExtension = file.name ? getFileNameAndExtension(file.name).extension : null
 
   if (file.isRemote) {
     // some remote providers do not support file types
-    return file.type ? file.type : extensionsToMime[fileExtension]
+    return file.type ? file.type : mimeTypes[fileExtension]
   }
 
-  // 2. if that’s no good, check if mime type is set in the file object
+  // check if mime type is set in the file object
   if (file.type) {
     return file.type
   }
 
-  // 3. if that’s no good, see if we can map extension to a mime type
-  if (fileExtension && extensionsToMime[fileExtension]) {
-    return extensionsToMime[fileExtension]
+  // see if we can map extension to a mime type
+  if (fileExtension && mimeTypes[fileExtension]) {
+    return mimeTypes[fileExtension]
   }
 
   // if all fails, well, return empty
@@ -189,104 +177,6 @@ function isObjectURL (url) {
   return url.indexOf('blob:') === 0
 }
 
-function getProportionalHeight (img, width) {
-  const aspect = img.width / img.height
-  return Math.round(width / aspect)
-}
-
-/**
- * Create a thumbnail for the given Uppy file object.
- *
- * @param {{data: Blob}} file
- * @param {number} width
- * @return {Promise}
- */
-function createThumbnail (file, targetWidth) {
-  const originalUrl = URL.createObjectURL(file.data)
-  const onload = new Promise((resolve, reject) => {
-    const image = new Image()
-    image.src = originalUrl
-    image.onload = () => {
-      URL.revokeObjectURL(originalUrl)
-      resolve(image)
-    }
-    image.onerror = () => {
-      // The onerror event is totally useless unfortunately, as far as I know
-      URL.revokeObjectURL(originalUrl)
-      reject(new Error('Could not create thumbnail'))
-    }
-  })
-
-  return onload.then((image) => {
-    const targetHeight = getProportionalHeight(image, targetWidth)
-    const canvas = resizeImage(image, targetWidth, targetHeight)
-    return canvasToBlob(canvas, 'image/png')
-  }).then((blob) => {
-    return URL.createObjectURL(blob)
-  })
-}
-
-/**
- * Resize an image to the target `width` and `height`.
- *
- * Returns a Canvas with the resized image on it.
- */
-function resizeImage (image, targetWidth, targetHeight) {
-  let sourceWidth = image.width
-  let sourceHeight = image.height
-
-  if (targetHeight < image.height / 2) {
-    const steps = Math.floor(Math.log(image.width / targetWidth) / Math.log(2))
-    const stepScaled = downScaleInSteps(image, steps)
-    image = stepScaled.image
-    sourceWidth = stepScaled.sourceWidth
-    sourceHeight = stepScaled.sourceHeight
-  }
-
-  const canvas = document.createElement('canvas')
-  canvas.width = targetWidth
-  canvas.height = targetHeight
-
-  const context = canvas.getContext('2d')
-  context.drawImage(image,
-    0, 0, sourceWidth, sourceHeight,
-    0, 0, targetWidth, targetHeight)
-
-  return canvas
-}
-
-/**
- * Downscale an image by 50% `steps` times.
- */
-function downScaleInSteps (image, steps) {
-  let source = image
-  let currentWidth = source.width
-  let currentHeight = source.height
-
-  for (let i = 0; i < steps; i += 1) {
-    const canvas = document.createElement('canvas')
-    const context = canvas.getContext('2d')
-    canvas.width = currentWidth / 2
-    canvas.height = currentHeight / 2
-    context.drawImage(source,
-      // The entire source image. We pass width and height here,
-      // because we reuse this canvas, and should only scale down
-      // the part of the canvas that contains the previous scale step.
-      0, 0, currentWidth, currentHeight,
-      // Draw to 50% size
-      0, 0, currentWidth / 2, currentHeight / 2)
-    currentWidth /= 2
-    currentHeight /= 2
-    source = canvas
-  }
-
-  return {
-    image: source,
-    sourceWidth: currentWidth,
-    sourceHeight: currentHeight
-  }
-}
-
 /**
  * Save a <canvas> element's content to a Blob object.
  *
@@ -560,7 +450,6 @@ module.exports = {
   getArrayBuffer,
   isPreviewSupported,
   isObjectURL,
-  createThumbnail,
   secondsToTime,
   dataURItoBlob,
   dataURItoFile,

+ 12 - 2
src/core/Utils.test.js

@@ -168,11 +168,21 @@ describe('core/utils', () => {
     })
 
     it('should determine the filetype from the extension', () => {
-      const file = {
+      const fileMP3 = {
         name: 'foo.mp3',
         data: 'sdfsfhfh329fhwihs'
       }
-      expect(utils.getFileType(file)).toEqual('audio/mp3')
+      const fileYAML = {
+        name: 'bar.yaml',
+        data: 'sdfsfhfh329fhwihs'
+      }
+      const fileMKV = {
+        name: 'bar.mkv',
+        data: 'sdfsfhfh329fhwihs'
+      }
+      expect(utils.getFileType(fileMP3)).toEqual('audio/mp3')
+      expect(utils.getFileType(fileYAML)).toEqual('text/yaml')
+      expect(utils.getFileType(fileMKV)).toEqual('video/x-matroska')
     })
 
     it('should fail gracefully if unable to detect', () => {

+ 36 - 0
src/core/mime-types.js

@@ -0,0 +1,36 @@
+module.exports = {
+  'md': 'text/markdown',
+  'markdown': 'text/markdown',
+  'mp4': 'video/mp4',
+  'mp3': 'audio/mp3',
+  'svg': 'image/svg+xml',
+  'jpg': 'image/jpeg',
+  'png': 'image/png',
+  'gif': 'image/gif',
+  'yaml': 'text/yaml',
+  'yml': 'text/yaml',
+  'csv': 'text/csv',
+  'avi': 'video/x-msvideo',
+  'mks': 'video/x-matroska',
+  'mkv': 'video/x-matroska',
+  'mov': 'video/quicktime',
+  'doc': 'application/msword',
+  'docm': 'application/vnd.ms-word.document.macroenabled.12',
+  'docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
+  'dot': 'application/msword',
+  'dotm': 'application/vnd.ms-word.template.macroenabled.12',
+  'dotx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.template',
+  'xla': 'application/vnd.ms-excel',
+  'xlam': 'application/vnd.ms-excel.addin.macroenabled.12',
+  'xlc': 'application/vnd.ms-excel',
+  'xlf': 'application/x-xliff+xml',
+  'xlm': 'application/vnd.ms-excel',
+  'xls': 'application/vnd.ms-excel',
+  'xlsb': 'application/vnd.ms-excel.sheet.binary.macroenabled.12',
+  'xlsm': 'application/vnd.ms-excel.sheet.macroenabled.12',
+  'xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
+  'xlt': 'application/vnd.ms-excel',
+  'xltm': 'application/vnd.ms-excel.template.macroenabled.12',
+  'xltx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.template',
+  'xlw': 'application/vnd.ms-excel'
+}

+ 424 - 0
src/plugins/AwsS3/Multipart.js

@@ -0,0 +1,424 @@
+const Plugin = require('../../core/Plugin')
+const RequestClient = require('../../server/RequestClient')
+const UppySocket = require('../../core/UppySocket')
+const {
+  emitSocketProgress,
+  getSocketHost,
+  limitPromises
+} = require('../../core/Utils')
+const Uploader = require('./MultipartUploader')
+
+/**
+ * Create a wrapper around an event emitter with a `remove` method to remove
+ * all events that were added using the wrapped emitter.
+ */
+function createEventTracker (emitter) {
+  const events = []
+  return {
+    on (event, fn) {
+      events.push([ event, fn ])
+      return emitter.on(event, fn)
+    },
+    remove () {
+      events.forEach(([ event, fn ]) => {
+        emitter.off(event, fn)
+      })
+    }
+  }
+}
+
+function assertServerError (res) {
+  if (res && res.error) {
+    const error = new Error(res.message)
+    Object.assign(error, res.error)
+    throw error
+  }
+  return res
+}
+
+module.exports = class AwsS3Multipart extends Plugin {
+  constructor (uppy, opts) {
+    super(uppy, opts)
+    this.type = 'uploader'
+    this.id = 'AwsS3Multipart'
+    this.title = 'AWS S3 Multipart'
+    this.server = new RequestClient(uppy, opts)
+
+    const defaultOptions = {
+      timeout: 30 * 1000,
+      limit: 0,
+      createMultipartUpload: this.createMultipartUpload.bind(this),
+      listParts: this.listParts.bind(this),
+      prepareUploadPart: this.prepareUploadPart.bind(this),
+      abortMultipartUpload: this.abortMultipartUpload.bind(this),
+      completeMultipartUpload: this.completeMultipartUpload.bind(this)
+    }
+
+    this.opts = Object.assign({}, defaultOptions, opts)
+
+    this.upload = this.upload.bind(this)
+
+    if (typeof this.opts.limit === 'number' && this.opts.limit !== 0) {
+      this.limitRequests = limitPromises(this.opts.limit)
+    } else {
+      this.limitRequests = (fn) => fn
+    }
+
+    this.uploaders = Object.create(null)
+    this.uploaderEvents = Object.create(null)
+    this.uploaderSockets = Object.create(null)
+  }
+
+  /**
+   * Clean up all references for a file's upload: the MultipartUploader instance,
+   * any events related to the file, and the uppy-server WebSocket connection.
+   */
+  resetUploaderReferences (fileID) {
+    if (this.uploaders[fileID]) {
+      this.uploaders[fileID].abort()
+      this.uploaders[fileID] = null
+    }
+    if (this.uploaderEvents[fileID]) {
+      this.uploaderEvents[fileID].remove()
+      this.uploaderEvents[fileID] = null
+    }
+    if (this.uploaderSockets[fileID]) {
+      this.uploaderSockets[fileID].close()
+      this.uploaderSockets[fileID] = null
+    }
+  }
+
+  assertHost () {
+    if (!this.opts.host) {
+      throw new Error('Expected a `host` option containing an uppy-server address.')
+    }
+  }
+
+  createMultipartUpload (file) {
+    this.assertHost()
+
+    return this.server.post('s3/multipart', {
+      filename: file.name,
+      type: file.type
+    }).then(assertServerError)
+  }
+
+  listParts (file, { key, uploadId }) {
+    this.assertHost()
+
+    const filename = encodeURIComponent(key)
+    return this.server.get(`s3/multipart/${uploadId}?key=${filename}`)
+      .then(assertServerError)
+  }
+
+  prepareUploadPart (file, { key, uploadId, number }) {
+    this.assertHost()
+
+    const filename = encodeURIComponent(key)
+    return this.server.get(`s3/multipart/${uploadId}/${number}?key=${filename}`)
+      .then(assertServerError)
+  }
+
+  completeMultipartUpload (file, { key, uploadId, parts }) {
+    this.assertHost()
+
+    const filename = encodeURIComponent(key)
+    const uploadIdEnc = encodeURIComponent(uploadId)
+    return this.server.post(`s3/multipart/${uploadIdEnc}/complete?key=${filename}`, { parts })
+      .then(assertServerError)
+  }
+
+  abortMultipartUpload (file, { key, uploadId }) {
+    this.assertHost()
+
+    const filename = encodeURIComponent(key)
+    const uploadIdEnc = encodeURIComponent(uploadId)
+    return this.server.delete(`s3/multipart/${uploadIdEnc}?key=${filename}`)
+      .then(assertServerError)
+  }
+
+  uploadFile (file) {
+    return new Promise((resolve, reject) => {
+      const upload = new Uploader(file.data, Object.assign({
+        // .bind to pass the file object to each handler.
+        createMultipartUpload: this.limitRequests(this.opts.createMultipartUpload.bind(this, file)),
+        listParts: this.limitRequests(this.opts.listParts.bind(this, file)),
+        prepareUploadPart: this.opts.prepareUploadPart.bind(this, file),
+        completeMultipartUpload: this.limitRequests(this.opts.completeMultipartUpload.bind(this, file)),
+        abortMultipartUpload: this.limitRequests(this.opts.abortMultipartUpload.bind(this, file)),
+
+        limit: this.opts.limit || 5,
+        onStart: (data) => {
+          const cFile = this.uppy.getFile(file.id)
+          this.uppy.setFileState(file.id, {
+            s3Multipart: Object.assign({}, cFile.s3Multipart, {
+              key: data.key,
+              uploadId: data.uploadId,
+              parts: []
+            })
+          })
+        },
+        onProgress: (bytesUploaded, bytesTotal) => {
+          this.uppy.emit('upload-progress', file, {
+            uploader: this,
+            bytesUploaded: bytesUploaded,
+            bytesTotal: bytesTotal
+          })
+        },
+        onError: (err) => {
+          this.uppy.log(err)
+          this.uppy.emit('upload-error', file, err)
+          err.message = `Failed because: ${err.message}`
+
+          this.resetUploaderReferences(file.id)
+          reject(err)
+        },
+        onSuccess: (result) => {
+          this.uppy.emit('upload-success', file, upload, result.location)
+
+          if (result.location) {
+            this.uppy.log('Download ' + upload.file.name + ' from ' + result.location)
+          }
+
+          this.resetUploaderReferences(file.id)
+          resolve(upload)
+        },
+        onPartComplete: (part) => {
+          // Store completed parts in state.
+          const cFile = this.uppy.getFile(file.id)
+          this.uppy.setFileState(file.id, {
+            s3Multipart: Object.assign({}, cFile.s3Multipart, {
+              parts: [
+                ...cFile.s3Multipart.parts,
+                part
+              ]
+            })
+          })
+
+          this.uppy.emit('s3-multipart:part-uploaded', cFile, part)
+        }
+      }, file.s3Multipart))
+
+      this.uploaders[file.id] = upload
+      this.uploaderEvents[file.id] = createEventTracker(this.uppy)
+
+      this.onFileRemove(file.id, (removed) => {
+        this.resetUploaderReferences(file.id)
+        resolve(`upload ${removed.id} was removed`)
+      })
+
+      this.onFilePause(file.id, (isPaused) => {
+        if (isPaused) {
+          upload.pause()
+        } else {
+          upload.start()
+        }
+      })
+
+      this.onPauseAll(file.id, () => {
+        upload.pause()
+      })
+
+      this.onCancelAll(file.id, () => {
+        upload.abort({ really: true })
+      })
+
+      this.onResumeAll(file.id, () => {
+        upload.start()
+      })
+
+      if (!file.isPaused) {
+        upload.start()
+      }
+
+      if (!file.isRestored) {
+        this.uppy.emit('upload-started', file, upload)
+      }
+    })
+  }
+
+  uploadRemote (file) {
+    this.resetUploaderReferences(file.id)
+
+    return new Promise((resolve, reject) => {
+      if (file.serverToken) {
+        return this.connectToServerSocket(file)
+          .then(() => resolve())
+          .catch(reject)
+      }
+
+      this.uppy.emit('upload-started', file)
+
+      fetch(file.remote.url, {
+        method: 'post',
+        credentials: 'include',
+        headers: {
+          'Accept': 'application/json',
+          'Content-Type': 'application/json'
+        },
+        body: JSON.stringify(Object.assign({}, file.remote.body, {
+          protocol: 's3-multipart',
+          size: file.data.size,
+          metadata: file.meta
+        }))
+      })
+      .then((res) => {
+        if (res.status < 200 || res.status > 300) {
+          return reject(res.statusText)
+        }
+
+        return res.json().then((data) => {
+          this.uppy.setFileState(file.id, { serverToken: data.token })
+          return this.uppy.getFile(file.id)
+        })
+      })
+      .then((file) => {
+        return this.connectToServerSocket(file)
+      })
+      .then(() => {
+        resolve()
+      })
+      .catch((err) => {
+        reject(new Error(err))
+      })
+    })
+  }
+
+  connectToServerSocket (file) {
+    return new Promise((resolve, reject) => {
+      const token = file.serverToken
+      const host = getSocketHost(file.remote.host)
+      const socket = new UppySocket({ target: `${host}/api/${token}` })
+      this.uploaderSockets[socket] = socket
+      this.uploaderEvents[file.id] = createEventTracker(this.uppy)
+
+      this.onFileRemove(file.id, (removed) => {
+        socket.send('pause', {})
+        resolve(`upload ${file.id} was removed`)
+      })
+
+      this.onFilePause(file.id, (isPaused) => {
+        socket.send(isPaused ? 'pause' : 'resume', {})
+      })
+
+      this.onPauseAll(file.id, () => socket.send('pause', {}))
+
+      this.onCancelAll(file.id, () => socket.send('pause', {}))
+
+      this.onResumeAll(file.id, () => {
+        if (file.error) {
+          socket.send('pause', {})
+        }
+        socket.send('resume', {})
+      })
+
+      this.onRetry(file.id, () => {
+        socket.send('pause', {})
+        socket.send('resume', {})
+      })
+
+      this.onRetryAll(file.id, () => {
+        socket.send('pause', {})
+        socket.send('resume', {})
+      })
+
+      if (file.isPaused) {
+        socket.send('pause', {})
+      }
+
+      socket.on('progress', (progressData) => emitSocketProgress(this, progressData, file))
+
+      socket.on('error', (errData) => {
+        this.uppy.emit('upload-error', file, new Error(errData.error))
+        reject(new Error(errData.error))
+      })
+
+      socket.on('success', (data) => {
+        this.uppy.emit('upload-success', file, data, data.url)
+        resolve()
+      })
+    })
+  }
+
+  upload (fileIDs) {
+    if (fileIDs.length === 0) return Promise.resolve()
+
+    const promises = fileIDs.map((id) => {
+      const file = this.uppy.getFile(id)
+      if (file.isRemote) {
+        return this.uploadRemote(file)
+      }
+      return this.uploadFile(file)
+    })
+
+    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)
+    })
+  }
+
+  onFilePause (fileID, cb) {
+    this.uploaderEvents[fileID].on('upload-pause', (targetFileID, isPaused) => {
+      if (fileID === targetFileID) {
+        // const isPaused = this.uppy.pauseResume(fileID)
+        cb(isPaused)
+      }
+    })
+  }
+
+  onRetry (fileID, cb) {
+    this.uploaderEvents[fileID].on('upload-retry', (targetFileID) => {
+      if (fileID === targetFileID) {
+        cb()
+      }
+    })
+  }
+
+  onRetryAll (fileID, cb) {
+    this.uploaderEvents[fileID].on('retry-all', (filesToRetry) => {
+      if (!this.uppy.getFile(fileID)) return
+      cb()
+    })
+  }
+
+  onPauseAll (fileID, cb) {
+    this.uploaderEvents[fileID].on('pause-all', () => {
+      if (!this.uppy.getFile(fileID)) return
+      cb()
+    })
+  }
+
+  onCancelAll (fileID, cb) {
+    this.uploaderEvents[fileID].on('cancel-all', () => {
+      if (!this.uppy.getFile(fileID)) return
+      cb()
+    })
+  }
+
+  onResumeAll (fileID, cb) {
+    this.uploaderEvents[fileID].on('resume-all', () => {
+      if (!this.uppy.getFile(fileID)) return
+      cb()
+    })
+  }
+
+  install () {
+    this.addResumableUploadsCapabilityFlag()
+    this.uppy.addUploader(this.upload)
+  }
+
+  uninstall () {
+    this.uppy.removeUploader(this.upload)
+  }
+}

+ 284 - 0
src/plugins/AwsS3/MultipartUploader.js

@@ -0,0 +1,284 @@
+const MB = 1024 * 1024
+
+const defaultOptions = {
+  limit: 1,
+  onStart () {},
+  onProgress () {},
+  onPartComplete () {},
+  onSuccess () {},
+  onError (err) {
+    throw err
+  }
+}
+
+function remove (arr, el) {
+  const i = arr.indexOf(el)
+  if (i !== -1) arr.splice(i, 1)
+}
+
+class MultipartUploader {
+  constructor (file, options) {
+    this.options = Object.assign({}, defaultOptions, options)
+    this.file = file
+
+    this.key = this.options.key || null
+    this.uploadId = this.options.uploadId || null
+    this.parts = this.options.parts || []
+
+    this.isPaused = false
+    this.chunks = null
+    this.chunkState = null
+    this.uploading = []
+
+    this._initChunks()
+  }
+
+  _initChunks () {
+    const chunks = []
+    const chunkSize = Math.max(Math.ceil(this.file.size / 10000), 5 * MB)
+
+    for (let i = 0; i < this.file.size; i += chunkSize) {
+      const end = Math.min(this.file.size, i + chunkSize)
+      chunks.push(this.file.slice(i, end))
+    }
+
+    this.chunks = chunks
+    this.chunkState = chunks.map(() => ({
+      uploaded: 0,
+      busy: false,
+      done: false
+    }))
+  }
+
+  _createUpload () {
+    return Promise.resolve().then(() =>
+      this.options.createMultipartUpload()
+    ).then((result) => {
+      const valid = typeof result === 'object' && result &&
+        typeof result.uploadId === 'string' &&
+        typeof result.key === 'string'
+      if (!valid) {
+        throw new TypeError(`AwsS3/Multipart: Got incorrect result from 'createMultipartUpload()', expected an object '{ uploadId, key }'.`)
+      }
+      return result
+    }).then((result) => {
+      this.key = result.key
+      this.uploadId = result.uploadId
+
+      this.options.onStart(result)
+    }).then(() => {
+      this._uploadParts()
+    }).catch((err) => {
+      this._onError(err)
+    })
+  }
+
+  _resumeUpload () {
+    return Promise.resolve().then(() =>
+      this.options.listParts({
+        uploadId: this.uploadId,
+        key: this.key
+      })
+    ).then((parts) => {
+      parts.forEach((part) => {
+        const i = part.PartNumber - 1
+        this.chunkState[i] = {
+          uploaded: part.Size,
+          etag: part.ETag,
+          done: true
+        }
+
+        // Only add if we did not yet know about this part.
+        if (!this.parts.some((p) => p.PartNumber === part.PartNumber)) {
+          this.parts.push({
+            PartNumber: part.PartNumber,
+            ETag: part.ETag
+          })
+        }
+      })
+      this._uploadParts()
+    }).catch((err) => {
+      this._onError(err)
+    })
+  }
+
+  _uploadParts () {
+    if (this.isPaused) return
+
+    const need = this.options.limit - this.uploading.length
+    if (need === 0) return
+
+    // All parts are uploaded.
+    if (this.chunkState.every((state) => state.done)) {
+      this._completeUpload()
+      return
+    }
+
+    const candidates = []
+    for (let i = 0; i < this.chunkState.length; i++) {
+      const state = this.chunkState[i]
+      if (state.done || state.busy) continue
+
+      candidates.push(i)
+      if (candidates.length >= need) {
+        break
+      }
+    }
+
+    candidates.forEach((index) => {
+      this._uploadPart(index)
+    })
+  }
+
+  _uploadPart (index) {
+    const body = this.chunks[index]
+    this.chunkState[index].busy = true
+
+    return Promise.resolve().then(() =>
+      this.options.prepareUploadPart({
+        key: this.key,
+        uploadId: this.uploadId,
+        body,
+        number: index + 1
+      })
+    ).then((result) => {
+      const valid = typeof result === 'object' && result &&
+        typeof result.url === 'string'
+      if (!valid) {
+        throw new TypeError(`AwsS3/Multipart: Got incorrect result from 'prepareUploadPart()', expected an object '{ url }'.`)
+      }
+      return result
+    }).then(({ url }) => {
+      this._uploadPartBytes(index, url)
+    })
+  }
+
+  _onPartProgress (index, sent, total) {
+    this.chunkState[index].uploaded = sent
+
+    const totalUploaded = this.chunkState.reduce((n, c) => n + c.uploaded, 0)
+    this.options.onProgress(totalUploaded, this.file.size)
+  }
+
+  _onPartComplete (index, etag) {
+    this.chunkState[index].etag = etag
+    this.chunkState[index].done = true
+
+    const part = {
+      PartNumber: index + 1,
+      ETag: etag
+    }
+    this.parts.push(part)
+
+    this.options.onPartComplete(part)
+
+    this._uploadParts()
+  }
+
+  _uploadPartBytes (index, url) {
+    const body = this.chunks[index]
+    const xhr = new XMLHttpRequest()
+    xhr.open('PUT', url, true)
+    xhr.responseType = 'text'
+
+    this.uploading.push(xhr)
+
+    xhr.upload.addEventListener('progress', (ev) => {
+      if (!ev.lengthComputable) return
+
+      this._onPartProgress(index, ev.loaded, ev.total)
+    })
+
+    xhr.addEventListener('abort', (ev) => {
+      remove(this.uploading, ev.target)
+      this.chunkState[index].busy = false
+    })
+
+    xhr.addEventListener('load', (ev) => {
+      remove(this.uploading, ev.target)
+      this.chunkState[index].busy = false
+
+      if (ev.target.status < 200 || ev.target.status >= 300) {
+        this._onError(new Error('Non 2xx'))
+        return
+      }
+
+      this._onPartProgress(index, body.size, body.size)
+
+      // NOTE This must be allowed by CORS.
+      const etag = ev.target.getResponseHeader('ETag')
+      if (etag === null) {
+        this._onError(new Error('AwsS3/Multipart: Could not read the ETag header. This likely means CORS is not configured correctly on the S3 Bucket. Seee https://uppy.io/docs/aws-s3-multipart#S3-Bucket-Configuration for instructions.'))
+        return
+      }
+
+      this._onPartComplete(index, etag)
+    })
+
+    xhr.addEventListener('error', (ev) => {
+      remove(this.uploading, ev.target)
+      this.chunkState[index].busy = false
+
+      const error = new Error('Unknown error')
+      error.source = ev.target
+      this._onError(error)
+    })
+
+    xhr.send(body)
+  }
+
+  _completeUpload () {
+    // Parts may not have completed uploading in sorted order, if limit > 1.
+    this.parts.sort((a, b) => a.PartNumber - b.PartNumber)
+
+    return Promise.resolve().then(() =>
+      this.options.completeMultipartUpload({
+        key: this.key,
+        uploadId: this.uploadId,
+        parts: this.parts
+      })
+    ).then((result) => {
+      this.options.onSuccess(result)
+    }, (err) => {
+      this._onError(err)
+    })
+  }
+
+  _abortUpload () {
+    this.options.abortMultipartUpload({
+      key: this.key,
+      uploadId: this.uploadId
+    })
+  }
+
+  _onError (err) {
+    this.options.onError(err)
+  }
+
+  start () {
+    this.isPaused = false
+    if (this.uploadId) {
+      this._resumeUpload()
+    } else {
+      this._createUpload()
+    }
+  }
+
+  pause () {
+    const inProgress = this.uploading.slice()
+    inProgress.forEach((xhr) => {
+      xhr.abort()
+    })
+    this.isPaused = true
+  }
+
+  abort (opts = {}) {
+    const really = opts.really || false
+
+    if (!really) return this.pause()
+
+    this._abortUpload()
+  }
+}
+
+module.exports = MultipartUploader

+ 13 - 8
src/plugins/Dashboard/ActionBrowseTagline.js

@@ -11,25 +11,30 @@ class ActionBrowseTagline extends Component {
   }
 
   render () {
+    const browse = (
+      <button type="button" class="uppy-Dashboard-browse" onclick={this.handleClick}>
+        {this.props.i18n('browse')}
+      </button>
+    )
+
     // empty value="" on file input, so that the input is cleared after a file is selected,
     // because Uppy will be handling the upload and so we can select same file
     // after removing — otherwise browser thinks it’s already selected
     return (
       <span>
         {this.props.acquirers.length === 0
-          ? this.props.i18n('dropPaste')
-          : this.props.i18n('dropPasteImport')
-        } <button type="button" class="uppy-Dashboard-browse" onclick={this.handleClick}>
-          {this.props.i18n('browse')}
-        </button>
+          ? this.props.i18nArray('dropPaste', { browse })
+          : this.props.i18nArray('dropPasteImport', { browse })
+        }
         <input class="uppy-Dashboard-input"
-          hidden="true"
+          hidden
           aria-hidden="true"
-          tabindex="-1"
+          tabindex={-1}
           type="file"
           name="files[]"
-          multiple="true"
+          multiple={this.props.maxNumberOfFiles !== 1}
           onchange={this.props.handleInputChange}
+          accept={this.props.allowedFileTypes}
           value=""
           ref={(input) => {
             this.input = input

+ 6 - 6
src/plugins/Dashboard/Dashboard.js

@@ -8,11 +8,11 @@ const { h } = require('preact')
 // http://dev.edenspiekermann.com/2016/02/11/introducing-accessible-modal-dialog
 // https://github.com/ghosh/micromodal
 
-const renderInnerPanel = (props) => {
+const PanelContent = (props) => {
   return <div style={{ width: '100%', height: '100%' }}>
     <div class="uppy-DashboardContent-bar">
       <div class="uppy-DashboardContent-title">
-        {props.i18n('importFrom')} {props.activePanel ? props.activePanel.name : null}
+        {props.i18n('importFrom', { name: props.activePanel.name })}
       </div>
       <button class="uppy-DashboardContent-back"
         type="button"
@@ -23,8 +23,8 @@ const renderInnerPanel = (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="uppy-Dashboard-poweredByIcon" width="12" height="12" viewBox="0 0 12 12" xmlns="http://www.w3.org/2000/svg">
-    <path fill-rule="nonzero" d="M8.57 7.554v4.149H3.424V7.554H0L6 0l6 7.554H8.57z" />
+  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">
+    <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>
 }
 
@@ -43,7 +43,7 @@ module.exports = function Dashboard (props) {
       aria-label={!props.inline ? props.i18n('dashboardWindowTitle') : props.i18n('dashboardTitle')}
       onpaste={props.handlePaste}>
 
-      <div class="uppy-Dashboard-overlay" tabindex="-1" onclick={props.handleClickOutside} />
+      <div class="uppy-Dashboard-overlay" tabindex={-1} onclick={props.handleClickOutside} />
 
       <div class="uppy-Dashboard-inner"
         aria-modal={!props.inline && 'true'}
@@ -73,7 +73,7 @@ module.exports = function Dashboard (props) {
             role="tabpanel"
             id={props.activePanel && `uppy-DashboardContent-panel--${props.activePanel.id}`}
             aria-hidden={props.activePanel ? 'false' : 'true'}>
-            {props.activePanel && renderInnerPanel(props)}
+            {props.activePanel && <PanelContent {...props} />}
           </div>
 
           <div class="uppy-Dashboard-progressindicators">

+ 16 - 10
src/plugins/Dashboard/FileCard.js

@@ -56,13 +56,21 @@ module.exports = class FileCard extends Component {
   }
 
   render () {
+    if (!this.props.fileCardFor) {
+      return <div class="uppy-DashboardFileCard" aria-hidden />
+    }
+
     const file = this.props.files[this.props.fileCardFor]
 
-    return <div class="uppy-DashboardFileCard" aria-hidden={!this.props.fileCardFor}>
-      {this.props.fileCardFor &&
-        <div style="width: 100%; height: 100%;">
+    return (
+      <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">{this.props.i18n('editing')} <span class="uppy-DashboardContent-titleFile">{file.meta ? file.meta.name : file.name}</span></h2>
+            <h2 class="uppy-DashboardContent-title">
+              {this.props.i18nArray('editing', {
+                file: <span class="uppy-DashboardContent-titleFile">{file.meta ? file.meta.name : file.name}</span>
+              })}
+            </h2>
             <button class="uppy-DashboardContent-back" type="button" title={this.props.i18n('finishEditingFile')}
               onclick={this.handleSave}>{this.props.i18n('done')}</button>
           </div>
@@ -79,16 +87,14 @@ module.exports = class FileCard extends Component {
             <div class="uppy-Dashboard-actions">
               <button class="uppy-u-reset uppy-c-btn uppy-c-btn-primary uppy-Dashboard-actionsBtn"
                 type="button"
-                title={this.props.i18n('finishEditingFiles')}
-                onclick={this.handleSave}>Save changes</button>
+                onclick={this.handleSave}>{this.props.i18n('saveChanges')}</button>
               <button class="uppy-u-reset uppy-c-btn uppy-c-btn-link uppy-Dashboard-actionsBtn"
                 type="button"
-                title={this.props.i18n('finishEditingFiles')}
-                onclick={this.handleCancel}>Cancel</button>
+                onclick={this.handleCancel}>{this.props.i18n('cancel')}</button>
             </div>
           </div>
         </div>
-      }
-    </div>
+      </div>
+    )
   }
 }

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

@@ -104,7 +104,7 @@ module.exports = function fileItem (props) {
         {file.source && <div class="uppy-DashboardItem-sourceIcon">
             {acquirers.map(acquirer => {
               if (acquirer.id === file.source) {
-                return <span title={`${props.i18n('fileSource')}: ${acquirer.name}`}>
+                return <span title={props.i18n('fileSource', { name: acquirer.name })}>
                   {acquirer.icon()}
                 </span>
               }

+ 29 - 26
src/plugins/Dashboard/FileList.js

@@ -15,35 +15,38 @@ module.exports = (props) => {
     {noFiles &&
       <div class="uppy-Dashboard-bgIcon">
         <div class="uppy-Dashboard-dropFilesTitle">
-          {h(ActionBrowseTagline, {
-            acquirers: props.acquirers,
-            handleInputChange: props.handleInputChange,
-            i18n: props.i18n
-          })}
+          <ActionBrowseTagline
+            acquirers={props.acquirers}
+            handleInputChange={props.handleInputChange}
+            i18n={props.i18n}
+            i18nArray={props.i18nArray}
+            allowedFileTypes={props.allowedFileTypes}
+            maxNumberOfFiles={props.maxNumberOfFiles}
+          />
         </div>
         { props.note && <div class="uppy-Dashboard-note">{props.note}</div> }
       </div>
     }
-    {Object.keys(props.files).map((fileID) => {
-      return FileItem({
-        acquirers: props.acquirers,
-        file: props.files[fileID],
-        toggleFileCard: props.toggleFileCard,
-        showProgressDetails: props.showProgressDetails,
-        info: props.info,
-        log: props.log,
-        i18n: props.i18n,
-        removeFile: props.removeFile,
-        pauseUpload: props.pauseUpload,
-        cancelUpload: props.cancelUpload,
-        retryUpload: props.retryUpload,
-        hideCancelButton: props.hideCancelButton,
-        hideRetryButton: props.hideRetryButton,
-        resumableUploads: props.resumableUploads,
-        isWide: props.isWide,
-        showLinkToFileUploadResult: props.showLinkToFileUploadResult,
-        metaFields: props.metaFields
-      })
-    })}
+    {Object.keys(props.files).map((fileID) => (
+      <FileItem
+        acquirers={props.acquirers}
+        file={props.files[fileID]}
+        toggleFileCard={props.toggleFileCard}
+        showProgressDetails={props.showProgressDetails}
+        info={props.info}
+        log={props.log}
+        i18n={props.i18n}
+        removeFile={props.removeFile}
+        pauseUpload={props.pauseUpload}
+        cancelUpload={props.cancelUpload}
+        retryUpload={props.retryUpload}
+        hideCancelButton={props.hideCancelButton}
+        hideRetryButton={props.hideRetryButton}
+        resumableUploads={props.resumableUploads}
+        isWide={props.isWide}
+        showLinkToFileUploadResult={props.showLinkToFileUploadResult}
+        metaFields={props.metaFields}
+      />
+    ))}
   </ul>
 }

+ 8 - 6
src/plugins/Dashboard/Tabs.js

@@ -23,7 +23,8 @@ class Tabs extends Component {
             <ActionBrowseTagline
               acquirers={this.props.acquirers}
               handleInputChange={this.props.handleInputChange}
-              i18n={this.props.i18n} />
+              i18n={this.props.i18n}
+              i18nArray={this.props.i18nArray} />
           </div>
         </div>
       )
@@ -38,18 +39,19 @@ class Tabs extends Component {
           <button type="button"
             class="uppy-DashboardTab-btn"
             role="tab"
-            tabindex="0"
+            tabindex={0}
             onclick={this.handleClick}>
             {localIcon()}
             <div class="uppy-DashboardTab-name">{this.props.i18n('myDevice')}</div>
           </button>
           <input class="uppy-Dashboard-input"
-            hidden="true"
+            hidden
             aria-hidden="true"
-            tabindex="-1"
+            tabindex={-1}
             type="file"
             name="files[]"
-            multiple="true"
+            multiple={this.props.maxNumberOfFiles !== 1}
+            accept={this.props.allowedFileTypes}
             onchange={this.props.handleInputChange}
             value=""
             ref={(input) => { this.input = input }} />
@@ -59,7 +61,7 @@ class Tabs extends Component {
             <button class="uppy-DashboardTab-btn"
               type="button"
               role="tab"
-              tabindex="0"
+              tabindex={0}
               aria-controls={`uppy-DashboardContent-panel--${target.id}`}
               aria-selected={this.props.activePanel.id === target.id}
               onclick={() => this.props.showPanel(target.id)}>

+ 13 - 8
src/plugins/Dashboard/index.js

@@ -41,23 +41,25 @@ module.exports = class Dashboard extends Plugin {
         selectToUpload: 'Select files to upload',
         closeModal: 'Close Modal',
         upload: 'Upload',
-        importFrom: 'Import from',
+        importFrom: 'Import from %{name}',
         dashboardWindowTitle: 'Uppy Dashboard Window (Press escape to close)',
         dashboardTitle: 'Uppy Dashboard',
         copyLinkToClipboardSuccess: 'Link copied to clipboard',
         copyLinkToClipboardFallback: 'Copy the URL below',
         copyLink: 'Copy link',
-        fileSource: 'File source',
+        fileSource: 'File source: %{name}',
         done: 'Done',
         name: 'Name',
         removeFile: 'Remove file',
         editFile: 'Edit file',
-        editing: 'Editing',
+        editing: 'Editing %{file}',
         finishEditingFile: 'Finish editing file',
+        saveChanges: 'Save changes',
+        cancel: 'Cancel',
         localDisk: 'Local Disk',
         myDevice: 'My Device',
-        dropPasteImport: 'Drop files here, paste, import from one of the locations above or',
-        dropPaste: 'Drop files here, paste or',
+        dropPasteImport: 'Drop files here, paste, import from one of the locations above or %{browse}',
+        dropPaste: 'Drop files here, paste or %{browse}',
         browse: 'browse',
         fileProgress: 'File progress: upload speed and ETA',
         numberOfSelectedFiles: 'Number of selected files',
@@ -117,6 +119,7 @@ module.exports = class Dashboard extends Plugin {
 
     this.translator = new Translator({locale: this.locale})
     this.i18n = this.translator.translate.bind(this.translator)
+    this.i18nArray = this.translator.translateArray.bind(this.translator)
 
     this.openModal = this.openModal.bind(this)
     this.closeModal = this.closeModal.bind(this)
@@ -242,8 +245,8 @@ module.exports = class Dashboard extends Plugin {
       document.body.classList.add('uppy-Dashboard-isOpen')
     }
 
+    this.rerender(this.uppy.getState())
     this.updateDashboardElWidth()
-    // this.setFocusToFirstNode()
     this.setFocusToBrowse()
   }
 
@@ -364,7 +367,6 @@ module.exports = class Dashboard extends Plugin {
 
   handleDrop (files) {
     this.uppy.log('[Dashboard] Files were dropped')
-    console.log(files)
 
     files.forEach((file) => {
       this.uppy.addFile({
@@ -470,6 +472,7 @@ module.exports = class Dashboard extends Plugin {
       hideAllPanels: this.hideAllPanels,
       log: this.uppy.log,
       i18n: this.i18n,
+      i18nArray: this.i18nArray,
       addFile: this.uppy.addFile,
       removeFile: this.uppy.removeFile,
       info: this.uppy.info,
@@ -490,7 +493,9 @@ module.exports = class Dashboard extends Plugin {
       proudlyDisplayPoweredByUppy: this.opts.proudlyDisplayPoweredByUppy,
       currentWidth: pluginState.containerWidth,
       isWide: pluginState.containerWidth > 400,
-      isTargetDOMEl: this.isTargetDOMEl
+      isTargetDOMEl: this.isTargetDOMEl,
+      allowedFileTypes: this.uppy.opts.restrictions.allowedFileTypes,
+      maxNumberOfFiles: this.uppy.opts.restrictions.maxNumberOfFiles
     })
   }
 

+ 23 - 7
src/plugins/DragDrop/index.js

@@ -17,7 +17,7 @@ module.exports = class DragDrop extends Plugin {
 
     const defaultLocale = {
       strings: {
-        dropHereOr: 'Drop files here or',
+        dropHereOr: 'Drop files here or %{browse}',
         browse: 'browse'
       }
     }
@@ -26,7 +26,6 @@ module.exports = class DragDrop extends Plugin {
     const defaultOpts = {
       target: null,
       inputName: 'files[]',
-      allowMultipleFiles: true,
       width: '100%',
       height: '100%',
       note: null,
@@ -45,6 +44,7 @@ module.exports = class DragDrop extends Plugin {
     // i18n
     this.translator = new Translator({locale: this.locale})
     this.i18n = this.translator.translate.bind(this.translator)
+    this.i18nArray = this.translator.translateArray.bind(this.translator)
 
     // Bind `this` to class methods
     this.handleDrop = this.handleDrop.bind(this)
@@ -104,11 +104,21 @@ module.exports = class DragDrop extends Plugin {
   }
 
   render (state) {
-    const DragDropClass = `uppy-Root uppy-DragDrop-container ${this.isDragDropSupported ? 'is-dragdrop-supported' : ''}`
+    /* http://tympanus.net/codrops/2015/09/15/styling-customizing-file-inputs-smart-way/ */
+    const hiddenInputStyle = {
+      width: '0.1px',
+      height: '0.1px',
+      opacity: 0,
+      overflow: 'hidden',
+      position: 'absolute',
+      zIndex: -1
+    }
+    const DragDropClass = `uppy-Root uppy-DragDrop-container ${this.isDragDropSupported ? 'uppy-DragDrop--is-dragdrop-supported' : ''}`
     const DragDropStyle = {
       width: this.opts.width,
       height: this.opts.height
     }
+    const restrictions = this.uppy.opts.restrictions
 
     // empty value="" on file input, so that the input is cleared after a file is selected,
     // because Uppy will be handling the upload and so we can select same file
@@ -120,14 +130,20 @@ module.exports = class DragDrop extends Plugin {
             <path d="M11 10V0H5v10H2l6 6 6-6h-3zm0 0" fill-rule="evenodd" />
           </svg>
           <label class="uppy-DragDrop-label">
-            <input class="uppy-DragDrop-input"
+            <input style={hiddenInputStyle}
+              class="uppy-DragDrop-input"
               type="file"
               name={this.opts.inputName}
-              multiple={this.opts.allowMultipleFiles}
-              ref={(input) => { this.input = input }}
+              multiple={restrictions.maxNumberOfFiles !== 1}
+              accept={restrictions.allowedFileTypes}
+              ref={(input) => {
+                this.input = input
+              }}
               onchange={this.handleInputChange}
               value="" />
-            {this.i18n('dropHereOr')} <span class="uppy-DragDrop-dragText">{this.i18n('browse')}</span>
+            {this.i18nArray('dropHereOr', {
+              browse: <span class="uppy-DragDrop-dragText">{this.i18n('browse')}</span>
+            })}
           </label>
           <span class="uppy-DragDrop-note">{this.opts.note}</span>
         </div>

+ 6 - 6
src/plugins/Dropbox/icons.js

@@ -1,14 +1,14 @@
 const { h } = require('preact')
 
 module.exports = {
-  folder: () => {
-    return <svg aria-hidden="true" class="UppyIcon" style="width:16px;margin-right:3px" viewBox="0 0 276.157 276.157">
+  folder: () => (
+    <svg aria-hidden="true" class="UppyIcon" style={{ width: 16, marginRight: 3 }} viewBox="0 0 276.157 276.157">
       <path d="M273.08 101.378c-3.3-4.65-8.86-7.32-15.254-7.32h-24.34V67.59c0-10.2-8.3-18.5-18.5-18.5h-85.322c-3.63 0-9.295-2.875-11.436-5.805l-6.386-8.735c-4.982-6.814-15.104-11.954-23.546-11.954H58.73c-9.292 0-18.638 6.608-21.737 15.372l-2.033 5.752c-.958 2.71-4.72 5.37-7.596 5.37H18.5C8.3 49.09 0 57.39 0 67.59v167.07c0 .886.16 1.73.443 2.52.152 3.306 1.18 6.424 3.053 9.064 3.3 4.652 8.86 7.32 15.255 7.32h188.487c11.395 0 23.27-8.425 27.035-19.18l40.677-116.188c2.11-6.035 1.43-12.164-1.87-16.816zM18.5 64.088h8.864c9.295 0 18.64-6.607 21.738-15.37l2.032-5.75c.96-2.712 4.722-5.373 7.597-5.373h29.565c3.63 0 9.295 2.876 11.437 5.806l6.386 8.735c4.982 6.815 15.104 11.954 23.546 11.954h85.322c1.898 0 3.5 1.602 3.5 3.5v26.47H69.34c-11.395 0-23.27 8.423-27.035 19.178L15 191.23V67.59c0-1.898 1.603-3.5 3.5-3.5zm242.29 49.15l-40.676 116.188c-1.674 4.78-7.812 9.135-12.877 9.135H18.75c-1.447 0-2.576-.372-3.02-.997-.442-.625-.422-1.814.057-3.18l40.677-116.19c1.674-4.78 7.812-9.134 12.877-9.134h188.487c1.448 0 2.577.372 3.02.997.443.625.423 1.814-.056 3.18z" />
     </svg>
-  },
-  file: () => {
-    return <svg aria-hidden="true" class="UppyIcon" width="11" height="14.5" viewBox="0 0 44 58">
+  ),
+  file: () => (
+    <svg aria-hidden="true" class="UppyIcon" width={11} height={14.5} viewBox="0 0 44 58">
       <path d="M27.437.517a1 1 0 0 0-.094.03H4.25C2.037.548.217 2.368.217 4.58v48.405c0 2.212 1.82 4.03 4.03 4.03H39.03c2.21 0 4.03-1.818 4.03-4.03V15.61a1 1 0 0 0-.03-.28 1 1 0 0 0 0-.093 1 1 0 0 0-.03-.032 1 1 0 0 0 0-.03 1 1 0 0 0-.032-.063 1 1 0 0 0-.03-.063 1 1 0 0 0-.032 0 1 1 0 0 0-.03-.063 1 1 0 0 0-.032-.03 1 1 0 0 0-.03-.063 1 1 0 0 0-.063-.062l-14.593-14a1 1 0 0 0-.062-.062A1 1 0 0 0 28 .708a1 1 0 0 0-.374-.157 1 1 0 0 0-.156 0 1 1 0 0 0-.03-.03l-.003-.003zM4.25 2.547h22.218v9.97c0 2.21 1.82 4.03 4.03 4.03h10.564v36.438a2.02 2.02 0 0 1-2.032 2.032H4.25c-1.13 0-2.032-.9-2.032-2.032V4.58c0-1.13.902-2.032 2.03-2.032zm24.218 1.345l10.375 9.937.75.718H30.5c-1.13 0-2.032-.9-2.032-2.03V3.89z" />
     </svg>
-  }
+  )
 }

+ 4 - 0
src/plugins/Dropbox/index.js

@@ -68,6 +68,10 @@ module.exports = class Dropbox extends Plugin {
     }
   }
 
+  getUsername (data) {
+    return data.user_email
+  }
+
   isFolder (item) {
     return item['.tag'] === 'folder'
   }

+ 5 - 2
src/plugins/FileInput.js

@@ -19,7 +19,6 @@ module.exports = class FileInput extends Plugin {
     // Default options
     const defaultOptions = {
       target: null,
-      allowMultipleFiles: true,
       pretty: true,
       inputName: 'files[]',
       locale: defaultLocale
@@ -60,6 +59,7 @@ module.exports = class FileInput extends Plugin {
   }
 
   render (state) {
+    /* http://tympanus.net/codrops/2015/09/15/styling-customizing-file-inputs-smart-way/ */
     const hiddenInputStyle = {
       width: '0.1px',
       height: '0.1px',
@@ -69,6 +69,8 @@ module.exports = class FileInput extends Plugin {
       zIndex: -1
     }
 
+    const restrictions = this.uppy.opts.restrictions
+
     // empty value="" on file input, so that the input is cleared after a file is selected,
     // because Uppy will be handling the upload and so we can select same file
     // after removing — otherwise browser thinks it’s already selected
@@ -78,7 +80,8 @@ module.exports = class FileInput extends Plugin {
         type="file"
         name={this.opts.inputName}
         onchange={this.handleInputChange}
-        multiple={this.opts.allowMultipleFiles}
+        multiple={restrictions.maxNumberOfFiles !== 1}
+        accept={restrictions.allowedFileTypes}
         ref={(input) => { this.input = input }}
         value="" />
       {this.opts.pretty &&

+ 12 - 0
src/plugins/GoogleDrive/index.js

@@ -63,6 +63,18 @@ module.exports = class GoogleDrive extends Plugin {
     }
   }
 
+  getUsername (data) {
+    for (const item of data.items) {
+      if (item.userPermission.role === 'owner') {
+        for (const owner of item.owners) {
+          if (owner.isAuthenticatedUser) {
+            return owner.emailAddress
+          }
+        }
+      }
+    }
+  }
+
   isFolder (item) {
     return item.mimeType === 'application/vnd.google-apps.folder'
   }

+ 4 - 0
src/plugins/Instagram/index.js

@@ -71,6 +71,10 @@ module.exports = class Instagram extends Plugin {
     }
   }
 
+  getUsername (data) {
+    return data.data[0].user.username
+  }
+
   isFolder (item) {
     return false
   }

+ 1 - 0
src/plugins/StatusBar/index.js

@@ -27,6 +27,7 @@ module.exports = class StatusBar extends Plugin {
         paused: 'Paused',
         error: 'Error',
         retry: 'Retry',
+        cancel: 'Cancel',
         pressToRetry: 'Press to retry',
         retryUpload: 'Retry upload',
         resumeUpload: 'Resume upload',

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

@@ -12,6 +12,10 @@ function defaultGetAssemblyOptions (file, options) {
   }
 }
 
+const UPPY_SERVER = 'https://api2.transloadit.com/uppy-server'
+// Regex used to check if an uppy-server address is run by Transloadit.
+const TL_UPPY_SERVER = /https?:\/\/api2(?:-\w+)?\.transloadit\.com\/uppy-server/
+
 /**
  * Upload files to Transloadit using Tus.
  */
@@ -181,23 +185,19 @@ module.exports = class Transloadit extends Plugin {
           endpoint: assembly.tus_url
         })
 
-        // Set uppy server location.
-        // we only add this, if 'file' has the attribute remote, because
-        // this is the criteria to identify remote files. If we add it without
-        // the check, then the file automatically becomes a remote file.
-        // @TODO: this is quite hacky. Please fix this later
-        let remote
-        if (file.remote) {
+        // Set uppy server location. We only add this, if 'file' has the attribute
+        // remote, because this is the criteria to identify remote files.
+        // We only replace the hostname for Transloadit's uppy-servers, so that
+        // people can self-host them while still using Transloadit for encoding.
+        let remote = file.remote
+        if (file.remote && TL_UPPY_SERVER.test(file.remote)) {
           let newHost = assembly.uppyserver_url
-          // remove tailing slash
-          if (newHost.endsWith('/')) {
-            newHost = newHost.slice(0, -1)
-          }
           let path = file.remote.url.replace(file.remote.host, '')
+          // remove tailing slash
+          newHost = newHost.replace(/\/$/, '')
           // remove leading slash
-          if (path.startsWith('/')) {
-            path = path.slice(1)
-          }
+          path = path.replace(/^\//, '')
+
           remote = Object.assign({}, file.remote, {
             host: newHost,
             url: `${newHost}/${path}`
@@ -860,3 +860,5 @@ module.exports = class Transloadit extends Plugin {
     })
   }
 }
+
+module.exports.UPPY_SERVER = UPPY_SERVER

+ 0 - 1
src/plugins/Transloadit/index.test.js

@@ -147,7 +147,6 @@ describe('Transloadit', () => {
         throw new Error('should not create Assembly')
       }
     })
-    uppy.run()
 
     return uppy.upload()
   })

+ 42 - 23
src/plugins/Tus.js

@@ -4,7 +4,8 @@ const UppySocket = require('../core/UppySocket')
 const {
   emitSocketProgress,
   getSocketHost,
-  settle
+  settle,
+  limitPromises
 } = require('../core/Utils')
 require('whatwg-fetch')
 
@@ -61,12 +62,20 @@ module.exports = class Tus extends Plugin {
       resume: true,
       autoRetry: true,
       useFastRemoteRetry: true,
+      limit: 0,
       retryDelays: [0, 1000, 3000, 5000]
     }
 
     // merge default options with the ones set by user
     this.opts = Object.assign({}, defaultOptions, opts)
 
+    // Simultaneous upload limiting is shared across all uploads with this plugin.
+    if (typeof this.opts.limit === 'number' && this.opts.limit !== 0) {
+      this.limitUploads = limitPromises(this.opts.limit)
+    } else {
+      this.limitUploads = (fn) => fn
+    }
+
     this.uploaders = Object.create(null)
     this.uploaderEvents = Object.create(null)
     this.uploaderSockets = Object.create(null)
@@ -157,7 +166,21 @@ module.exports = class Tus extends Plugin {
         this.resetUploaderReferences(file.id)
         resolve(upload)
       }
-      optsTus.metadata = file.meta
+
+      const copyProp = (obj, srcProp, destProp) => {
+        if (
+          Object.prototype.hasOwnProperty.call(obj, srcProp) &&
+          !Object.prototype.hasOwnProperty.call(obj, destProp)
+        ) {
+          obj[destProp] = obj[srcProp]
+        }
+      }
+
+      // tusd uses metadata fields 'filetype' and 'filename'
+      const meta = Object.assign({}, file.meta)
+      copyProp(meta, 'type', 'filetype')
+      copyProp(meta, 'name', 'filename')
+      optsTus.metadata = meta
 
       const upload = new tus.Upload(file.data, optsTus)
       this.uploaders[file.id] = upload
@@ -194,9 +217,6 @@ module.exports = class Tus extends Plugin {
       if (!file.isPaused) {
         upload.start()
       }
-      if (!file.isRestored) {
-        this.uppy.emit('upload-started', file, upload)
-      }
     })
   }
 
@@ -218,8 +238,6 @@ module.exports = class Tus extends Plugin {
           .catch(reject)
       }
 
-      this.uppy.emit('upload-started', file)
-
       fetch(file.remote.url, {
         method: 'post',
         credentials: 'include',
@@ -242,7 +260,7 @@ module.exports = class Tus extends Plugin {
 
         return res.json().then((data) => {
           this.uppy.setFileState(file.id, { serverToken: data.token })
-          file = this.getFile(file.id)
+          file = this.uppy.getFile(file.id)
           return file
         })
       })
@@ -328,10 +346,6 @@ module.exports = class Tus extends Plugin {
     })
   }
 
-  getFile (fileID) {
-    return this.uppy.state.files[fileID]
-  }
-
   updateFile (file) {
     const files = Object.assign({}, this.uppy.state.files, {
       [file.id]: file
@@ -340,7 +354,7 @@ module.exports = class Tus extends Plugin {
   }
 
   onReceiveUploadUrl (file, uploadURL) {
-    const currentFile = this.getFile(file.id)
+    const currentFile = this.uppy.getFile(file.id)
     if (!currentFile) return
     // Only do the update if we didn't have an upload URL yet,
     // or resume: false in options
@@ -407,23 +421,28 @@ module.exports = class Tus extends Plugin {
   }
 
   uploadFiles (files) {
-    const promises = files.map((file, index) => {
-      const current = parseInt(index, 10) + 1
+    const actions = files.map((file, i) => {
+      const current = parseInt(i, 10) + 1
       const total = files.length
 
       if (file.error) {
-        return Promise.reject(new Error(file.error))
-      }
-
-      this.uppy.log(`uploading ${current} of ${total}`)
-
-      if (file.isRemote) {
-        return this.uploadRemote(file, current, total)
+        return () => Promise.reject(new Error(file.error))
+      } else if (file.isRemote) {
+        // We emit upload-started here, so that it's also emitted for files
+        // that have to wait due to the `limit` option.
+        this.uppy.emit('upload-started', file)
+        return this.uploadRemote.bind(this, file, current, total)
       } else {
-        return this.upload(file, current, total)
+        this.uppy.emit('upload-started', file)
+        return this.upload.bind(this, file, current, total)
       }
     })
 
+    const promises = actions.map((action) => {
+      const limitedAction = this.limitUploads(action)
+      return limitedAction()
+    })
+
     return settle(promises)
   }
 

+ 62 - 0
src/plugins/Url/index.js

@@ -3,6 +3,7 @@ const Translator = require('../../core/Translator')
 const { h } = require('preact')
 const { RequestClient } = require('../../server')
 const UrlUI = require('./UrlUI.js')
+const { toArray } = require('../../core/Utils')
 require('whatwg-fetch')
 
 /**
@@ -54,6 +55,11 @@ module.exports = class Url extends Plugin {
     // Bind all event handlers for referencability
     this.getMeta = this.getMeta.bind(this)
     this.addFile = this.addFile.bind(this)
+    this.handleDrop = this.handleDrop.bind(this)
+    this.handleDragOver = this.handleDragOver.bind(this)
+    this.handleDragLeave = this.handleDragLeave.bind(this)
+
+    this.handlePaste = this.handlePaste.bind(this)
 
     this.server = new RequestClient(uppy, {host: this.opts.host})
   }
@@ -144,6 +150,52 @@ module.exports = class Url extends Plugin {
       })
   }
 
+  handleDrop (e) {
+    e.preventDefault()
+    if (e.dataTransfer.items) {
+      const items = toArray(e.dataTransfer.items)
+      items.forEach((item) => {
+        if (item.kind === 'string' && item.type === 'text/uri-list') {
+          item.getAsString((url) => {
+            this.uppy.log(`[URL] Adding file from dropped url: ${url}`)
+            this.addFile(url)
+          })
+        }
+      })
+    }
+  }
+
+  handleDragOver (e) {
+    e.preventDefault()
+    this.el.classList.add('drag')
+  }
+
+  handleDragLeave (e) {
+    e.preventDefault()
+    this.el.classList.remove('drag')
+  }
+
+  handlePaste (e) {
+    if (e.clipboardData.items) {
+      const items = toArray(e.clipboardData.items)
+
+      // When a file is pasted, it appears as two items: file name string, then
+      // the file itself; Url then treats file name string as URL, which is wrong.
+      // This makes sure Url ignores paste event if it contains an actual file
+      const hasFiles = items.filter(item => item.kind === 'file').length > 0
+      if (hasFiles) return
+
+      items.forEach((item) => {
+        if (item.kind === 'string' && item.type === 'text/plain') {
+          item.getAsString((url) => {
+            this.uppy.log(`[URL] Adding file from pasted url: ${url}`)
+            this.addFile(url)
+          })
+        }
+      })
+    }
+  }
+
   render (state) {
     return <UrlUI
       i18n={this.i18n}
@@ -155,9 +207,19 @@ module.exports = class Url extends Plugin {
     if (target) {
       this.mount(target, this)
     }
+
+    this.el.addEventListener('drop', this.handleDrop)
+    this.el.addEventListener('dragover', this.handleDragOver)
+    this.el.addEventListener('dragleave', this.handleDragLeave)
+    this.el.addEventListener('paste', this.handlePaste)
   }
 
   uninstall () {
+    this.el.removeEventListener('drop', this.handleDrop)
+    this.el.removeEventListener('dragover', this.handleDragOver)
+    this.el.removeEventListener('dragleave', this.handleDragLeave)
+    this.el.removeEventListener('paste', this.handlePaste)
+
     this.unmount()
   }
 }

+ 12 - 6
src/scss/_dashboard.scss

@@ -34,8 +34,11 @@
   max-height: 100%; /* no !important */
   width: 100%; /* no !important */
   height: 100%; /* no !important */
+  min-width: 300px;
+  min-height: 400px;
   outline: none;
   border: 1px solid rgba($color-gray, 0.2);
+  margin-bottom: 30px;
 
   .uppy-Dashboard--modal & {
     z-index: $zIndex-3;
@@ -49,13 +52,13 @@
 }
 
 .uppy-Dashboard-poweredBy {
-  position: absolute;
-  right: 4px;
-  bottom: -23px;
+  display: block;
   font-size: 11px;
   color: rgba($color-gray, 0.8);
   text-align: right;
   text-decoration: none;
+  padding-top: 8px;
+  padding-right: 2px;
 }
 
   .uppy-Dashboard--modal .uppy-Dashboard-poweredBy {
@@ -74,11 +77,14 @@
   stroke: $color-gray;
   fill: none;
   margin-left: 1px;
-  margin-right: 2px;
+  margin-right: 1px;
+  position: relative;
+  top: 1px;
+  opacity: 0.9;
 }
 
   .uppy-Dashboard--modal .uppy-Dashboard-poweredByIcon {
-    stroke: none;
+    stroke: transparent;
     fill: $color-uppy-pink;
   }
 
@@ -952,7 +958,7 @@
 }
 
   .uppy-Dashboard--wide .uppy-Dashboard-actions {
-    height: 75px;
+    height: 65px;
   }
 
 .uppy-Dashboard-actionsBtn {

+ 2 - 12
src/scss/_dragdrop.scss

@@ -23,7 +23,7 @@
     margin-bottom: 17px;
   }
   
-    .uppy-DragDrop-container.is-dragdrop-supported {
+    .uppy-DragDrop--is-dragdrop-supported {
       border: 2px dashed;
       border-color: lighten($color-gray, 10%);
     }
@@ -41,16 +41,6 @@
       fill: $color-gray;
     }
   
-  /* http://tympanus.net/codrops/2015/09/15/styling-customizing-file-inputs-smart-way/ */
-  .uppy-DragDrop-input {
-    width: 0.1px;
-    height: 0.1px;
-    opacity: 0;
-    overflow: hidden;
-    position: absolute;
-    z-index: -1;
-  }
-  
   .uppy-DragDrop-label {
     display: block;
     cursor: pointer;
@@ -67,4 +57,4 @@
     color: $color-cornflower-blue;
   }
 
-// }
+// }

+ 9 - 2
src/scss/_provider.scss

@@ -1,6 +1,7 @@
 .uppy-Provider-auth,
 .uppy-Provider-error,
-.uppy-Provider-loading {
+.uppy-Provider-loading,
+.uppy-Provider-empty {
   display: flex;
   align-items: center;
   justify-content: center;
@@ -81,9 +82,15 @@
 }
 
 .uppy-ProviderBrowser-user {
-  margin: 16px 0;
+  margin: 0 8px 0 0;
 }
 
+  .uppy-ProviderBrowser-user:after {
+    content: '\00B7';
+    position: relative;
+    left: 4px;
+  }
+
 .uppy-ProviderBrowser-header {
   z-index: $zIndex-2;
   border-bottom: 1px solid lighten($color-asphalt-gray, 60%);

+ 15 - 0
src/server/RequestClient.js

@@ -58,4 +58,19 @@ module.exports = class RequestClient {
       // @todo validate response status before calling json
       .then((res) => res.json())
   }
+
+  delete (path, data) {
+    return fetch(`${this.hostname}/${path}`, {
+      method: 'delete',
+      credentials: 'include',
+      headers: {
+        'Accept': 'application/json',
+        'Content-Type': 'application/json'
+      },
+      body: data ? JSON.stringify(data) : null
+    })
+      .then(this.onReceiveResponse)
+      // @todo validate response status before calling json
+      .then((res) => res.json())
+  }
 }

+ 27 - 14
src/views/ProviderView/AuthView.js

@@ -1,28 +1,41 @@
 const LoaderView = require('./Loader')
 const { h, Component } = require('preact')
 
+class AuthBlock extends Component {
+  componentDidMount () {
+    this.connectButton.focus()
+  }
+
+  render () {
+    return <div class="uppy-Provider-auth">
+      <div class="uppy-Provider-authIcon">{this.props.pluginIcon()}</div>
+      <h1 class="uppy-Provider-authTitle">Please authenticate with <span class="uppy-Provider-authTitleName">{this.props.pluginName}</span><br /> to select files</h1>
+      <button
+        type="button"
+        class="uppy-u-reset uppy-c-btn uppy-c-btn-primary uppy-Provider-authBtn"
+        onclick={this.props.handleAuth}
+        ref={(el) => { this.connectButton = el }}
+      >
+        Connect to {this.props.pluginName}
+      </button>
+      {this.props.demo &&
+        <button class="uppy-u-reset uppy-c-btn uppy-c-btn-primary uppy-Provider-authBtn" onclick={this.props.handleDemoAuth}>Proceed with Demo Account</button>
+      }
+    </div>
+  }
+}
+
 class AuthView extends Component {
   componentDidMount () {
     this.props.checkAuth()
   }
 
   render () {
-    const AuthBlock = () => {
-      return <div class="uppy-Provider-auth">
-        <div class="uppy-Provider-authIcon">{this.props.pluginIcon()}</div>
-        <h1 class="uppy-Provider-authTitle">Please authenticate with <span class="uppy-Provider-authTitleName">{this.props.pluginName}</span><br /> to select files</h1>
-        <button type="button" class="uppy-u-reset uppy-c-btn uppy-c-btn-primary uppy-Provider-authBtn" onclick={this.props.handleAuth}>Connect to {this.props.pluginName}</button>
-        {this.props.demo &&
-          <button class="uppy-u-reset uppy-c-btn uppy-c-btn-primary uppy-Provider-authBtn" onclick={this.props.handleDemoAuth}>Proceed with Demo Account</button>
-        }
-      </div>
-    }
-
     return (
-      <div style="height: 100%;">
+      <div style={{ height: '100%' }}>
         {this.props.checkAuthInProgress
-          ? LoaderView()
-          : AuthBlock()
+          ? <LoaderView />
+          : <AuthBlock {...this.props} />
         }
       </div>
     )

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

@@ -22,6 +22,7 @@ module.exports = (props) => {
             directories: props.directories,
             title: props.title
           })}
+          <span class="uppy-ProviderBrowser-user">{props.username}</span>
           <button type="button" onclick={props.logout} class="uppy-ProviderBrowser-userLogout">Log out</button>
         </div>
       </div>
@@ -45,7 +46,8 @@ module.exports = (props) => {
         handleScroll: props.handleScroll,
         title: props.title,
         showTitles: props.showTitles,
-        getItemId: props.getItemId
+        getItemId: props.getItemId,
+        i18n: props.i18n
       })}
       <button class="UppyButton--circular UppyButton--blue uppy-ProviderBrowser-doneBtn"
         type="button"

+ 2 - 2
src/views/ProviderView/Item.js

@@ -22,7 +22,7 @@ module.exports = (props) => {
       <div class="uppy-ProviderBrowserItem-checkbox">
         <input type="checkbox"
           role="option"
-          tabindex="0"
+          tabindex={0}
           aria-label={`Select ${props.title}`}
           id={props.id}
           checked={props.isChecked}
@@ -39,7 +39,7 @@ module.exports = (props) => {
       <button type="button"
         class="uppy-ProviderBrowserItem-inner"
         aria-label={`Select ${props.title}`}
-        tabindex="0"
+        tabindex={0}
         onclick={handleItemClick}>
         {props.getItemIcon()} {props.showTitles && props.title}
       </button>

+ 3 - 11
src/views/ProviderView/ItemList.js

@@ -2,17 +2,9 @@ const Row = require('./Item')
 const { h } = require('preact')
 
 module.exports = (props) => {
-  // const headers = props.columns.map((column) => {
-  //   return html`
-  //     <th class="uppy-ProviderBrowserTable-headerColumn uppy-ProviderBrowserTable-column" onclick=${props.sortByTitle}>
-  //       ${column.name}
-  //     </th>
-  //   `
-  // })
-
-  // <thead class="uppy-ProviderBrowserTable-header">
-  //   <tr>${headers}</tr>
-  // </thead>
+  if (!props.folders.length && !props.files.length) {
+    return <div class="uppy-Provider-empty">{props.i18n('noFilesFound')}</div>
+  }
 
   return (
     <div class="uppy-ProviderBrowser-body">

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

@@ -131,6 +131,7 @@ module.exports = class ProviderView {
           updatedDirectories = state.directories.concat([{id, title: name || this.plugin.getItemName(res)}])
         }
 
+        this.username = this.username ? this.username : this.plugin.getUsername(res)
         this._updateFilesAndFolders(res, files, folders)
         this.plugin.setPluginState({ directories: updatedDirectories })
       },
@@ -565,6 +566,7 @@ module.exports = class ProviderView {
     }
 
     const browserProps = Object.assign({}, this.plugin.getPluginState(), {
+      username: this.username,
       getNextFolder: this.getNextFolder,
       getFolder: this.getFolder,
       addFile: this.addFile,
@@ -588,7 +590,8 @@ module.exports = class ProviderView {
       showTitles: this.opts.showTitles,
       showFilter: this.opts.showFilter,
       showBreadcrumbs: this.opts.showBreadcrumbs,
-      pluginIcon: this.plugin.icon
+      pluginIcon: this.plugin.icon,
+      i18n: this.plugin.uppy.i18n
     })
 
     return Browser(browserProps)

+ 1 - 5
test/endtoend/src/main.js

@@ -15,7 +15,6 @@ const uppyDragDrop = Uppy({
   })
   .use(ProgressBar, { target: '#uppyDragDrop-progress' })
   .use(Tus, { endpoint: 'https://master.tus.io/files/' })
-  .run()
 
 const uppyi18n = Uppy({
   id: 'uppyi18n',
@@ -25,14 +24,13 @@ const uppyi18n = Uppy({
     target: '#uppyi18n',
     locale: {
       strings: {
-        dropHereOr: 'Перенесите файлы сюда или',
+        dropHereOr: 'Перенесите файлы сюда или %{browse}',
         browse: 'выберите'
       }
     }
   })
   .use(ProgressBar, { target: '#uppyi18n-progress' })
   .use(XHRUpload, { endpoint: 'https://api2.transloadit.com' })
-  .run()
 
 const uppyDashboard = Uppy({
   id: 'uppyDashboard',
@@ -43,7 +41,6 @@ const uppyDashboard = Uppy({
     inline: true
   })
   .use(Tus, { endpoint: 'https://master.tus.io/files/' })
-  .run()
 
 function startXHRLimitTest (endpoint) {
   const uppy = Uppy({
@@ -53,7 +50,6 @@ function startXHRLimitTest (endpoint) {
   })
     .use(DragDrop, { target: '#uppyXhrLimit' })
     .use(XHRUpload, { endpoint, limit: 2 })
-    .run()
 
   uppy.uploadsStarted = 0
   uppy.uploadsComplete = 0

+ 1 - 2
website/src/api-usage-example.ejs

@@ -5,7 +5,6 @@ import Tus from 'uppy/lib/plugins/Tus'
 Uppy({ autoProceed: false })
   .use(Dashboard, { trigger: '#select-files' })
   .use(Tus, { endpoint: 'https://master.tus.io/files/' })
-  .run()
   .on('complete', (result) => {
     console.log('Upload result:', result)
-  })
+  })

+ 119 - 0
website/src/docs/aws-s3-multipart.md

@@ -0,0 +1,119 @@
+---
+type: docs
+order: 33
+title: "AwsS3Multipart"
+permalink: docs/aws-s3-multipart/
+---
+
+The `AwsS3Multipart` plugin can be used to upload files directly to an S3 bucket using S3's Multipart upload strategy. With this strategy, files are chopped up in parts of 5MB+ each, so they can be uploaded concurrently. It's also very reliable: if a single part fails to upload, only that 5MB has to be retried.
+
+```js
+const AwsS3Multipart = require('uppy/lib/plugins/AwsS3/Multipart')
+uppy.use(AwsS3Multipart, {
+  limit: 4,
+  host: 'https://uppy-server.myapp.net/'
+})
+```
+
+## Options
+
+### limit: 0
+
+The maximum amount of chunks to upload simultaneously. `0` means unlimited.
+
+### host: null
+
+The Uppy Server URL to use to proxy calls to the S3 Multipart API.
+
+### createMultipartUpload(file)
+
+A function that calls the S3 Multipart API to create a new upload. `file` is the file object from Uppy's state. The most relevant keys are `file.name` and `file.type`.
+
+Return a Promise for an object with keys:
+
+ - `uploadId` - The UploadID returned by S3.
+ - `key` - The object key for the file. This needs to be returned to allow it to be different from the `file.name`.
+
+The default implementation calls out to Uppy Server's S3 signing endpoints.
+
+### listParts({ uploadId, key })
+
+A function that calls the S3 Multipart API to list the parts of a file that have already been uploaded. Receives an object with keys:
+
+ - `uploadId` - The UploadID of this Multipart upload.
+ - `key` - The object key of this Multipart upload.
+
+Return a Promise for an array of S3 Part objects, as returned by the S3 Multipart API. Each object has keys:
+
+ - `PartNumber` - The index in the file of the uploaded part.
+ - `Size` - The size of the part in bytes.
+ - `ETag` - The ETag of the part, used to identify it when completing the multipart upload and combining all parts into a single file.
+
+The default implementation calls out to Uppy Server's S3 signing endpoints.
+
+### prepareUploadPart(partData)
+
+A function that generates a signed URL to upload a single part. The `partData` argument is an object with keys:
+
+ - `uploadId` - The UploadID of this Multipart upload.
+ - `key` - The object key in the S3 bucket.
+ - `body` - A [`Blob`](https://developer.mozilla.org/en-US/docs/Web/API/Blob) of this part's contents.
+ - `number` - The index of this part in the file (`PartNumber` in S3 terminology).
+
+Return a Promise for an object with keys:
+
+ - `url` - The presigned URL to upload a part. This can be generated using the S3 SDK like so:
+
+   ```js
+   sdkInstance.getSignedUrl('uploadPart', {
+     Bucket: 'target',
+     Key: partData.key,
+     UploadId: partData.uploadId,
+     PartNumber: partData.number,
+     Body: '', // Empty, because it's uploaded later
+     Expires: Date.now() + 5 * 60 * 1000
+   }, (err, url) => { /* there's the url! */ })
+   ```
+
+### abortMultipartUpload({ uploadId, key })
+
+A function that calls the S3 Multipart API to abort a Multipart upload, and delete all parts that have been uploaded so far. Receives an object with keys:
+
+ - `uploadId` - The UploadID of this Multipart upload.
+ - `key` - The object key of this Multipart upload.
+
+This is typically called when the user cancels an upload. Cancellation cannot fail in Uppy, so the result of this function is ignored.
+
+The default implementation calls out to Uppy Server's S3 signing endpoints.
+
+### completeMultipartUpload({ uploadId, key, parts })
+
+A function that calls the S3 Multipart API to complete a Multipart upload, combining all parts into a single object in the S3 bucket. Receives an object with keys:
+
+ - `uploadId` - The UploadID of this Multipart upload.
+ - `key` - The object key of this Multipart upload.
+ - `parts` - S3-style list of parts, an array of objects with `ETag` and `PartNumber` properties. This can be passed straight to S3's Multipart API.
+
+Return a Promise for an object with properties:
+
+ - `location` - **(Optional)** A publically accessible URL to the object in the S3 bucket.
+
+The default implementation calls out to Uppy Server's S3 signing endpoints.
+
+## S3 Bucket Configuration
+
+S3 buckets do not allow public uploads by default.  In order to allow Uppy to upload to a bucket directly, its CORS permissions need to be configured.
+
+This process is described in the [AwsS3 documentation](/docs/aws-s3/#S3-Bucket-configuration).
+
+On top of the configuration mentioned there, the `ETag` header must also be whitelisted:
+
+```xml
+<CORSRule>
+  <AllowedMethod>PUT</AllowedMethod>
+  <!-- ... all your existingCORS config goes here ... -->
+
+  <!-- The magic: -->
+  <ExposeHeader>ETag</ExposeHeader>
+</CORSRule>
+```

+ 25 - 1
website/src/docs/aws-s3.md

@@ -9,15 +9,26 @@ The `AwsS3` plugin can be used to upload files directly to an S3 bucket.
 Uploads can be signed using [uppy-server][uppy-server docs] or a custom signing function.
 
 ```js
+const AwsS3 = require('uppy/lib/plugins/AwsS3')
+const ms = require('ms')
+
 uppy.use(AwsS3, {
-  // Options
+  limit: 2,
+  timeout: ms('1 minute'),
+  host: 'https://uppy-server.myapp.com/'
 })
 ```
 
 There are broadly two ways to upload to S3 in a browser. A server can generate a presigned URL for a [PUT upload](https://docs.aws.amazon.com/AmazonS3/latest/API/RESTObjectPUT.html), or a server can generate form data for a [POST upload](https://docs.aws.amazon.com/AmazonS3/latest/API/RESTObjectPOST.html). uppy-server uses a POST upload. See [POST uPloads](#post-uploads) for some caveats if you would like to use POST uploads without uppy-server. See [Generating a presigned upload URL server-side](#example-presigned-url) for an example of a PUT upload.
 
+There is also a separate plugin for S3 Multipart uploads. Multipart in this sense is Amazon's proprietary chunked, resumable upload mechanism for large files. See the [AwsS3Multipart](/docs/aws-s3-multipart) documentation.
+
 ## Options
 
+### `id: 'AwsS3'`
+
+A unique identifier for this plugin. Defaults to `'AwsS3'`.
+
 ### `host`
 
 When using [uppy-server][uppy-server docs] to sign S3 uploads, set this option to the root URL of the uppy-server.
@@ -59,6 +70,19 @@ The default is 30 seconds.
 Limit the amount of uploads going on at the same time. This is passed through to [XHRUpload](/docs/xhrupload#limit-0); see its documentation page for details.
 Set to `0` to disable limiting.
 
+### `locale: {}`
+
+Localize text that is shown to the user.
+
+The default English strings are:
+
+```js
+strings: {
+  // Shown in the StatusBar while the upload is being signed.
+  preparingUpload: 'Preparing upload...'
+}
+```
+
 ## S3 Bucket configuration
 
 S3 buckets do not allow public uploads by default.

+ 76 - 47
website/src/docs/dashboard.md

@@ -14,6 +14,14 @@ Dashboard is a universal UI plugin for Uppy:
 - Progress: total and for individual files
 - Ability to pause/resume or cancel (depending on uploader plugin) individual or all files
 
+```js
+const Dashboard = require('uppy/lib/plugins/Dashboard')
+
+uppy.use(Dashboard, {
+  // Options
+})
+```
+
 [Try it live](/examples/dashboard/)
 
 ## Options
@@ -40,54 +48,15 @@ uppy.use(Dashboard, {
   disablePageScrollWhenModalOpen: true,
   proudlyDisplayPoweredByUppy: true,
   onRequestCloseModal: () => this.closeModal(),
-  locale: {
-    strings: {
-      selectToUpload: 'Select files to upload',
-      closeModal: 'Close Modal',
-      upload: 'Upload',
-      importFrom: 'Import from',
-      dashboardWindowTitle: 'Uppy Dashboard Window (Press escape to close)',
-      dashboardTitle: 'Uppy Dashboard',
-      copyLinkToClipboardSuccess: 'Link copied to clipboard.',
-      copyLinkToClipboardFallback: 'Copy the URL below',
-      copyLink: 'Copy link',
-      fileSource: 'File source',
-      done: 'Done',
-      name: 'Name',
-      removeFile: 'Remove file',
-      editFile: 'Edit file',
-      editing: 'Editing',
-      finishEditingFile: 'Finish editing file',
-      localDisk: 'Local Disk',
-      myDevice: 'My Device',
-      dropPasteImport: 'Drop files here, paste, import from one of the locations above or',
-      dropPaste: 'Drop files here, paste or',
-      browse: 'browse',
-      fileProgress: 'File progress: upload speed and ETA',
-      numberOfSelectedFiles: 'Number of selected files',
-      uploadAllNewFiles: 'Upload all new files',
-      emptyFolderAdded: 'No files were added from empty folder',
-      uploadComplete: 'Upload complete',
-      resumeUpload: 'Resume upload',
-      pauseUpload: 'Pause upload',
-      retryUpload: 'Retry upload',
-      uploadXFiles: {
-        0: 'Upload %{smart_count} file',
-        1: 'Upload %{smart_count} files'
-      },
-      uploadXNewFiles: {
-        0: 'Upload +%{smart_count} file',
-        1: 'Upload +%{smart_count} files'
-      },
-      folderAdded: {
-        0: 'Added %{smart_count} file from %{folder}',
-        1: 'Added %{smart_count} files from %{folder}'
-      }
-    }
-  }
+  locale: {}
 })
 ```
 
+### `id: 'Dashboard'`
+
+A unique identifier for this Dashboard. Defaults to `'Dashboard'`. Change this if you need multiple Dashboard instances.
+Plugins that are added by the Dashboard get unique IDs based on this ID, like `'Dashboard:StatusBar'` and `'Dashboard:Informer'`.
+
 ### `target: 'body'`
 
 Dashboard is rendered into `body` by default, because by default it’s hidden and only opened as a modal when `trigger` is clicked.
@@ -187,9 +156,69 @@ Dashboard ships with the `Informer` plugin that notifies when the browser is off
 
 Dashboard ships with `ThumbnailGenerator` plugin that adds small resized image thumbnails to images, for preview purposes only. If you want, you can disable the `ThumbnailGenerator` and/or provide your custom solution.
 
-### `locale`
+### `locale: {}`
+
+Localize text that is shown to the user.
+
+The default English strings are:
+
+```js
+strings: {
+  // When `inline: false`, used as the screen reader label for the button that closes the modal.
+  closeModal: 'Close Modal',
+  // Used as the header for import panels, eg. "Import from Google Drive"
+  importFrom: 'Import from %{name}',
+  // When `inline: false`, used as the screen reader label for the dashboard modal.
+  dashboardWindowTitle: 'Uppy Dashboard Window (Press escape to close)',
+  // When `inline: true`, used as the screen reader label for the dashboard area.
+  dashboardTitle: 'Uppy Dashboard',
+  // Shown in the Informer when a link to a file was copied to the clipboard.
+  copyLinkToClipboardSuccess: 'Link copied to clipboard.',
+  // Used when a link cannot be copied automatically—the user has to select the text from the
+  // input element below this string.
+  copyLinkToClipboardFallback: 'Copy the URL below',
+  // Used as the hover title and screen reader label for buttons that copy a file link.
+  copyLink: 'Copy link',
+  // Used as the hover title and screen reader label for file source icons. Eg. "File source: Dropbox"
+  fileSource: 'File source: %{name}',
+  // Used as the label for buttons that accept and close panels (remote providers or metadata editor)
+  done: 'Done',
+  // Used as the screen reader label for buttons that remove a file.
+  removeFile: 'Remove file',
+  // Used as the screen reader label for buttons that open the metadata editor panel for a file.
+  editFile: 'Edit file',
+  // Shown in the panel header for the metadata editor. Rendered as "Editing image.png".
+  editing: 'Editing %{file}',
+  // Used as the screen reader label for the button that saves metadata edits and returns to the
+  // file list view.
+  finishEditingFile: 'Finish editing file',
+  // Used as the label for the tab button that opens the system file selection dialog.
+  myDevice: 'My Device',
+  // Shown in the main dashboard area when no files have been selected, and one or more
+  // remote provider plugins are in use. %{browse} is replaced with a link that opens the system
+  // file selection dialog.
+  dropPasteImport: 'Drop files here, paste, import from one of the locations above or %{browse}',
+  // Shown in the main dashboard area when no files have been selected, and no provider
+  // plugins are in use. %{browse} is replaced with a link that opens the system
+  // file selection dialog.
+  dropPaste: 'Drop files here, paste or %{browse}',
+  // This string is clickable and opens the system file selection dialog.
+  browse: 'browse',
+  // Used as the hover text and screen reader label for file progress indicators when
+  // they have been fully uploaded.
+  uploadComplete: 'Upload complete',
+  // Used as the hover text and screen reader label for the buttons to resume paused uploads.
+  resumeUpload: 'Resume upload',
+  // Used as the hover text and screen reader label for the buttons to pause uploads.
+  pauseUpload: 'Pause upload',
+  // Used as the hover text and screen reader label for the buttons to retry failed uploads.
+  retryUpload: 'Retry upload'
+}
+```
+
+### `replaceTargetContent: false`
 
-See [general plugin options](/docs/plugins).
+Remove all children of the `target` element before mounting the Dashboard. By default, Uppy will append any UI to the `target` DOM element. This is the least dangerous option. However, there might be cases when you’d want to clear the container element before placing Uppy UI in there (for example, to provide a fallback `<form>` that will be shown if Uppy or JavaScript is not available). Set `replaceTargetContent: true` to clear the `target` before appending.
 
 ## Methods
 

+ 30 - 11
website/src/docs/dragdrop.md

@@ -7,6 +7,14 @@ permalink: docs/dragdrop/
 
 DragDrop renders a simple Drag and Drop area for file selection. Useful when you only want the local device as a file source, don’t need file previews and metadata editing UI, and the [Dashboard](/docs/dashboard/) feels like an overkill.
 
+```js
+const DragDrop = require('uppy/lib/plugins/DragDrop')
+
+uppy.use(DragDrop, {
+  // Options
+})
+```
+
 [Try it live](/examples/dragdrop/)
 
 ## Options
@@ -16,17 +24,17 @@ uppy.use(DragDrop, {
   target: null,
   width: '100%',
   height: '100%',
-  allowMultipleFiles: true,
   note: null,
-  locale: {
-    strings: {
-      dropHereOr: 'Drop files here or',
-      browse: 'browse'
-    }
-  }
+  locale: {}
 })
 ```
 
+> Note that certain [restrictions set in Uppy’s main options](/docs/uppy#restrictions), namely `maxNumberOfFiles` and `allowedFileTypes`, affect the system file picker dialog. If `maxNumberOfFiles: 1`, users will only be able to select one file, and `allowedFileTypes: ['video/*', '.gif']` means only videos or gifs (files with `.gif` extension) will be selectable.
+
+### `id: 'DragDrop'`
+
+A unique identifier for this DragDrop. Defaults to `'DragDrop'`. Use this if you need to add multiple DragDrop instances.
+
 ### `target: null`
 
 DOM element, CSS selector, or plugin to place the drag and drop area into.
@@ -39,11 +47,22 @@ Drag and drop area width, set in inline CSS, so feel free to use percentage, pix
 
 Drag and drop area height, set in inline CSS, so feel free to use percentage, pixels or other values that you like.
 
-### `allowMultipleFiles: true`
-
-Whether to allow user to select multiple files at once via the system file dialog.
-
 ### `note: null`
 
 Optionally specify a string of text that explains something about the upload for the user. This is a place to explain `restrictions` that are put in place. For example: `'Images and video only, 2–3 files, up to 1 MB'`.
 
+### `locale: {}`
+
+Localize text that is shown to the user.
+
+The default English strings are:
+
+```js
+strings: {
+  // Text to show on the droppable area.
+  // `%{browse}` is replaced with a link that opens the system file selection dialog.
+  dropHereOr: 'Drop here or %{browse}',
+  // Used as the label for the link that opens the system file selection dialog.
+  browse: 'browse'
+}
+```

+ 53 - 0
website/src/docs/dropbox.md

@@ -0,0 +1,53 @@
+---
+type: docs
+order: 51
+title: "Dropbox"
+permalink: docs/dropbox/
+---
+
+The Dropbox plugin lets users import files their Google Drive account.
+
+An Uppy Server instance is required for the Dropbox plugin to work. Uppy Server handles authentication with Dropbox, downloads the files, and uploads them to the destination. This saves the user bandwidth, especially helpful if they are on a mobile connection.
+
+```js
+const Dropbox = require('uppy/lib/plugins/Dropbox')
+
+uppy.use(Dropbox, {
+  // Options
+})
+```
+
+[Try live!](/examples/dashboard/)
+
+## Options
+
+```js
+uppy.use(Dropbox, {
+  target: Dashboard,
+  host: 'https://server.uppy.io/',
+})
+```
+
+### `id: 'Dropbox'`
+
+A unique identifier for this plugin. Defaults to `'Dropbox'`.
+
+### `target: null`
+
+DOM element, CSS selector, or plugin to mount the Dropbox provider into. This should normally be the Dashboard.
+
+### `host: null`
+
+URL to an Uppy Server instance.
+
+### `locale: {}`
+
+Localize text that is shown to the user.
+
+The default English strings are:
+
+```js
+strings: {
+  // TODO
+}
+```

+ 21 - 9
website/src/docs/fileinput.md

@@ -7,6 +7,14 @@ permalink: docs/fileinput/
 
 `FileInput` is the most barebones UI for selecting files—it shows a single button that, when clicked, opens up the browser's file selector.
 
+```js
+const XHRUpload = require('uppy/lib/plugins/XHRUpload')
+
+uppy.use(XHRUpload, {
+  // Options
+})
+```
+
 [Try it live](/examples/xhrupload) - The XHRUpload example uses a `FileInput`.
 
 ## Options
@@ -14,24 +22,22 @@ permalink: docs/fileinput/
 ```js
 uppy.use(FileInput, {
   target: null,
-  allowMultipleFiles: true,
   pretty: true,
   inputName: 'files[]',
   locale: {
-    strings: {
-      chooseFiles: 'Choose files'
-    }
   }
 })
 ```
 
-### `target: null`
+> Note that certain [restrictions set in Uppy’s main options](/docs/uppy#restrictions), namely `maxNumberOfFiles` and `allowedFileTypes`, affect the system file picker dialog. If `maxNumberOfFiles: 1`, users will only be able to select one file, and `allowedFileTypes: ['video/*', '.gif']` means only videos or gifs (files with `.gif` extension) will be selectable.
 
-DOM element, CSS selector, or plugin to mount the file input into.
+### `id: 'FileInput'`
 
-### `allowMultipleFiles: true`
+A unique identifier for this FileInput. Defaults to `'FileInput'`. Use this if you need to add multiple FileInput instances.
 
-Whether to allow the user to select multiple files at once.
+### `target: null`
+
+DOM element, CSS selector, or plugin to mount the file input into.
 
 ### `pretty: true`
 
@@ -43,4 +49,10 @@ The `name` attribute for the `<input type="file">` element.
 
 ### `locale: {}`
 
-Custom text to show on the button when `pretty` is true.
+When `pretty` is set, specify a custom label for the button.
+
+```js
+strings: {
+  chooseFiles: 'Choose files'
+}
+```

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

@@ -7,6 +7,14 @@ permalink: docs/form/
 
 Form plugin collects metadata from any specified `<form>` element, right before Uppy begins uploading/processing files. And then optionally appends results back to the form. Currently the appended result is a stringified version of a [`result`](docs/uppy/#uppy-upload) returned from `uppy.upload()` or `complete` event.
 
+```js
+const Form = require('uppy/lib/plugins/Form')
+
+uppy.use(Form, {
+  // Options
+})
+```
+
 ## Options
 
 ```js
@@ -19,6 +27,10 @@ uppy.use(Form, {
 })
 ```
 
+### `id: 'Form'`
+
+A unique identifier for this Form. Defaults to `'Form'`.
+
 ### `target: null`
 
 DOM element or CSS selector for the form element. Required for the plugin to work.
@@ -33,7 +45,7 @@ Whether to add upload/encoding results back to the form in an `<input name="uppy
 
 ### `resultName: 'uppyResult'`
 
-The `name` attribute for the `<input type="hidden">` where the result will be added. 
+The `name` attribute for the `<input type="hidden">` where the result will be added.
 
 ### `submitOnSuccess: false`
 

+ 0 - 1
website/src/docs/golden-retriever.md

@@ -22,7 +22,6 @@ require('uppy/lib/plugins/GoldenRetriever/ServiceWorker.js')
 const GoldenRetriever = require('uppy/lib/plugins/GoldenRetriever')
 
 uppy.use(GoldenRetriever, {serviceWorker: true})
-uppy.run()
 
 if ('serviceWorker' in navigator) {
   navigator.serviceWorker

+ 53 - 0
website/src/docs/google-drive.md

@@ -0,0 +1,53 @@
+---
+type: docs
+order: 52
+title: "GoogleDrive"
+permalink: docs/google-drive/
+---
+
+The GoogleDrive plugin lets users import files their Google Drive account.
+
+An Uppy Server instance is required for the GoogleDrive plugin to work. Uppy Server handles authentication with Google, downloads files from the Drive and uploads them to the destination. This saves the user bandwidth, especially helpful if they are on a mobile connection.
+
+```js
+const GoogleDrive = require('uppy/lib/plugins/GoogleDrive')
+
+uppy.use(GoogleDrive, {
+  // Options
+})
+```
+
+[Try live!](/examples/dashboard/)
+
+## Options
+
+```js
+uppy.use(GoogleDrive, {
+  target: Dashboard,
+  host: 'https://server.uppy.io/',
+})
+```
+
+### `id: 'GoogleDrive'`
+
+A unique identifier for this plugin. Defaults to `'GoogleDrive'`.
+
+### `target: null`
+
+DOM element, CSS selector, or plugin to mount the GoogleDrive provider into. This should normally be the Dashboard.
+
+### `host: null`
+
+URL to an Uppy Server instance.
+
+### `locale: {}`
+
+Localize text that is shown to the user.
+
+The default English strings are:
+
+```js
+strings: {
+  // TODO
+}
+```

+ 2 - 4
website/src/docs/index.md

@@ -20,7 +20,6 @@ const uppy = Uppy({ autoProceed: false })
     trigger: '#select-files'
   })
   .use(Tus, {endpoint: 'https://master.tus.io/files/'})
-  .run()
  
 uppy.on('complete', (result) => {
   console.log(`Upload complete! We’ve uploaded these files: ${result.successful}`)
@@ -48,12 +47,12 @@ Alternatively, you can also use a pre-built bundle from Transloadit's CDN: Edgly
 1\. Add a script to the bottom of `<body>`:
 
 ``` html
-<script src="https://transloadit.edgly.net/releases/uppy/v0.24.3/dist/uppy.min.js"></script>
+<script src="https://transloadit.edgly.net/releases/uppy/v0.24.4/dist/uppy.min.js"></script>
 ```
 
 2\. Add CSS to `<head>`:
 ``` html
-<link href="https://transloadit.edgly.net/releases/uppy/v0.24.3/dist/uppy.min.css" rel="stylesheet">
+<link href="https://transloadit.edgly.net/releases/uppy/v0.24.4/dist/uppy.min.css" rel="stylesheet">
 ```
 
 3\. Initialize:
@@ -63,7 +62,6 @@ Alternatively, you can also use a pre-built bundle from Transloadit's CDN: Edgly
   var uppy = Uppy.Core({ autoProceed: false })
   uppy.use(Uppy.DragDrop, { target: '#drag-drop-area' })
   uppy.use(Uppy.Tus, { endpoint: 'https://master.tus.io/files/' })
-  uppy.run()
 </script>
 ```
 

+ 16 - 0
website/src/docs/informer.md

@@ -7,10 +7,22 @@ permalink: docs/informer/
 
 The Informer is a pop-up bar for showing notifications. When plugins have some exciting news (or error) to share, they can show a notification here.
 
+```js
+const Informer = require('uppy/lib/plugins/Informer')
+
+uppy.use(Informer, {
+  // Options
+})
+```
+
 [Try it live](/examples/dashboard/) - The Informer is included in the Dashboard by default.
 
 ## Options
 
+### `id: 'Informer'`
+
+A unique identifier for this Informer. Defaults to `'Informer'`. Use this if you need multiple Informer instances.
+
 ### `target: null`
 
 DOM element, CSS selector, or plugin to mount the informer into.
@@ -29,3 +41,7 @@ uppy.use(Informer, {
   }
 })
 ```
+
+### `replaceTargetContent: false`
+
+Remove all children of the `target` element before mounting the Informer. By default, Uppy will append any UI to the `target` DOM element. This is the least dangerous option. However, you may have some fallback HTML inside the `target` element in case JavaScript or Uppy is not available. In that case you can set `replaceTargetContent: true` to clear the `target` before appending.

+ 53 - 0
website/src/docs/instagram.md

@@ -0,0 +1,53 @@
+---
+type: docs
+order: 53
+title: "Instagram"
+permalink: docs/instagram/
+---
+
+The Instagram plugin lets users import files their Google Drive account.
+
+An Uppy Server instance is required for the Instagram plugin to work. Uppy Server handles authentication with Instagram, downloads the pictures and videos, and uploads them to the destination. This saves the user bandwidth, especially helpful if they are on a mobile connection.
+
+```js
+const Instagram = require('uppy/lib/plugins/Instagram')
+
+uppy.use(Instagram, {
+  // Options
+})
+```
+
+[Try live!](/examples/dashboard/)
+
+## Options
+
+```js
+uppy.use(Instagram, {
+  target: Dashboard,
+  host: 'https://server.uppy.io/',
+})
+```
+
+### `id: 'Instagram'`
+
+A unique identifier for this plugin. Defaults to `'Instagram'`.
+
+### `target: null`
+
+DOM element, CSS selector, or plugin to mount the Instagram provider into. This should normally be the Dashboard.
+
+### `host: null`
+
+URL to an Uppy Server instance.
+
+### `locale: {}`
+
+Localize text that is shown to the user.
+
+The default English strings are:
+
+```js
+strings: {
+  // TODO
+}
+```

+ 6 - 49
website/src/docs/plugins.md

@@ -11,8 +11,12 @@ Plugins are what makes Uppy useful: they help select, manipulate and upload file
   - [Dashboard](/docs/dashboard) — full featured sleek UI with file previews, metadata editing, upload/pause/resume/cancel buttons and more. Includes `StatusBar` and `Informer` plugins by default
   - [DragDrop](/docs/dragdrop) — plain and simple drag and drop area
   - [FileInput](/docs/fileinput) — even more plain and simple, just a button
-  - [Provider Plugins](#Provider-Plugins) (remote sources that work through [Uppy Server](/docs/uppy-server/)): Instagram, GoogleDrive, Dropbox, Url (direct link)
   - [Webcam](/docs/webcam) — upload selfies or audio / video recordings
+  - [Provider Plugins](/docs/providers) (remote sources that work through [Uppy Server](/docs/uppy-server/))
+    - [Dropbox](/docs/dropbox) – import files from Dropbox
+    - [GoogleDrive](/docs/google-drive) – import files from Google Drive
+    - [Instagram](/docs/instagram) – import files from Instagram
+    - [Url](/docs/url) – import files from any public URL
 - **Uploaders:**
   - [Tus](/docs/tus) — uploads using the [tus](https://tus.io) resumable upload protocol
   - [XHRUpload](/docs/xhrupload) — classic multipart form uploads or binary uploads using XMLHTTPRequest
@@ -62,24 +66,6 @@ uppy.use(GoogleDrive, {target: Dashboard})
 
 In the example above the `Dashboard` gets rendered into an element with ID `uppy`, while `GoogleDrive` is rendered into the `Dashboard` itself.
 
-### `endpoint`
-
-Used by uploader plugins, such as [Tus](/docs/tus) and [XHRUpload](/docs/xhrupload). Expects a `string` with a url that will be used for file uploading.
-
-### `host`
-
-Used by remote provider plugins, such as Google Drive, Instagram or Dropbox. Specifies the url to your running `uppy-server`. This allows uppy to know what server to connect to when server related operations are required by the provider plugin.
-
-```js
-// for Google Drive
-const GoogleDrive = require('uppy/lib/plugins/GoogleDrive')
-uppy.use(GoogleDrive, {target: Dashboard, host: 'http://localhost:3020'})
-```
-
-### `replaceTargetContent: false`
-
-By default Uppy will append any UI to a DOM element, if such element is specified as a `target`. This default is the least dangerous option. However, there might be cases when you’d want to clear the container element before placing Uppy UI in there (for example, to provide a fallback `<form>` that will be shown if Uppy or JavaScript is not loaded/supported on the page). Set `replaceTargetContent: true` to clear the `target` before appending.
-
 ### `locale: {}`
 
 Same as with Uppy.Core’s setting from above, this allows you to override plugin’s locale string, so that instead of `Select files` in English, your users will see `Выберите файлы` in Russian. Example:
@@ -97,33 +83,4 @@ See plugin documentation pages for other plugin-specific options.
 
 ## Provider Plugins
 
-The Provider plugins help you connect to your accounts with remote file providers such as [Dropbox](https://dropbox.com), [Google Drive](https://drive.google.com), [Instagram](https://instagram.com) and remote urls (import a file by pasting a direct link to it). Because this requires server to server communication, they work tightly with [uppy-server](https://github.com/transloadit/uppy-server) to manage the server to server authorization for your account. Virtually most of the communication (file download/upload) is done on the server-to-server end, so this saves you the stress of data consumption on the client.
-
-As of now, the supported providers are **Dropbox**, **GoogleDrive**, **Instagram**, and **Url**.
-
-Usage of the Provider plugins is not that different from any other *acquirer* plugin, except that it takes an extra option `host`, which specifies the url to your running `uppy-server`. This allows Uppy to know what server to connect to when server related operations are required by the provider plugin. Here's a quick example.
-
-```js
-const Uppy = require('uppy/lib/core')
-const Dashboard = require('uppy/lib/plugins/Dashboard')
-const uppy = Uppy()
-uppy.use(Dashboard, {
-  trigger: '#pick-files'
-})
-
-// for Google Drive
-const GoogleDrive = require('uppy/lib/plugins/GoogleDrive')
-uppy.use(GoogleDrive, {target: Dashboard, host: 'http://localhost:3020'})
-
-// for Dropbox
-const Dropbox = require('uppy/lib/plugins/Dropbox')
-uppy.use(Dropbox, {target: Dashboard, host: 'http://localhost:3020'})
-
-// for Instagram
-const Instagram = require('uppy/lib/plugins/Instagram')
-uppy.use(Instagram, {target: Dashboard, host: 'http://localhost:3020'})
-
-// for Url
-const Url = require('uppy/lib/plugins/Url')
-uppy.use(Url, {target: Dashboard, host: 'http://localhost:3020'})
-```
+See the [Provider Plugins](/docs/providers) documentation page for information on provider plugins.

+ 16 - 0
website/src/docs/progressbar.md

@@ -7,6 +7,14 @@ permalink: docs/progressbar/
 
 ProgressBar is a minimalist plugin that shows the current upload progress in a thin bar element, similar to the ones used by YouTube and GitHub when navigating between pages.
 
+```js
+const ProgressBar = require('uppy/lib/plugins/ProgressBar')
+
+uppy.use(ProgressBar, {
+  // Options
+})
+```
+
 [Try it live](/examples/dragdrop/) - The DragDrop example uses ProgressBars to show progress.
 
 ## Options
@@ -19,6 +27,10 @@ uppy.use(ProgressBar, {
 })
 ```
 
+### `id: 'ProgressBar'`
+
+A unique identifier for this ProgressBar. Defaults to `'ProgressBar'`. Use this if you need to add multiple ProgressBar instances.
+
 ### `target: null`
 
 DOM element, CSS selector, or plugin to mount the progress bar into.
@@ -37,3 +49,7 @@ uppy.use(ProgressBar, {
 ### `hideAfterFinish: true`
 
 When true, hides the progress bar after the upload has finished. If false, it remains visible.
+
+### `replaceTargetContent: false`
+
+Remove all children of the `target` element before mounting the ProgressBar. By default, Uppy will append any UI to the `target` DOM element. This is the least dangerous option. However, you may have some fallback HTML inside the `target` element in case JavaScript or Uppy is not available. In that case you can set `replaceTargetContent: true` to clear the `target` before appending.

+ 37 - 0
website/src/docs/providers.md

@@ -0,0 +1,37 @@
+---
+title: "Provider Plugins"
+type: docs
+permalink: docs/providers/
+order: 50
+---
+
+The Provider plugins help you connect to your accounts with remote file providers such as [Dropbox](https://dropbox.com), [Google Drive](https://drive.google.com), [Instagram](https://instagram.com) and remote urls (import a file by pasting a direct link to it). Because this requires server to server communication, they work tightly with [uppy-server](https://github.com/transloadit/uppy-server) to manage the server to server authorization for your account. Almost all of the communication (file download/upload) is done on the server-to-server end, so this saves you the stress and bills of data consumption on the client.
+
+As of now, the supported providers are [**Dropbox**](/docs/dropbox), [**GoogleDrive**](/docs/google-drive), [**Instagram**](/docs/instagram), and [**Url**](/docs/url).
+
+Usage of the Provider plugins is not that different from any other *acquirer* plugin, except that it takes an extra option `host`, which specifies the url to your running `uppy-server`. This allows Uppy to know what server to connect to when server related operations are required by the provider plugin. Here's a quick example.
+
+```js
+const Uppy = require('uppy/lib/core')
+const Dashboard = require('uppy/lib/plugins/Dashboard')
+const uppy = Uppy()
+uppy.use(Dashboard, {
+  trigger: '#pick-files'
+})
+
+// for Google Drive
+const GoogleDrive = require('uppy/lib/plugins/GoogleDrive')
+uppy.use(GoogleDrive, {target: Dashboard, host: 'http://localhost:3020'})
+
+// for Dropbox
+const Dropbox = require('uppy/lib/plugins/Dropbox')
+uppy.use(Dropbox, {target: Dashboard, host: 'http://localhost:3020'})
+
+// for Instagram
+const Instagram = require('uppy/lib/plugins/Instagram')
+uppy.use(Instagram, {target: Dashboard, host: 'http://localhost:3020'})
+
+// for Url
+const Url = require('uppy/lib/plugins/Url')
+uppy.use(Url, {target: Dashboard, host: 'http://localhost:3020'})
+```

+ 79 - 0
website/src/docs/react-dashboard-modal.md

@@ -0,0 +1,79 @@
+---
+title: "&lt;DashboardModal />"
+type: docs
+permalink: docs/react/dashboard-modal/
+order: 65
+---
+
+The `<DashboardModal />` component wraps the [Dashboard][] plugin, allowing control over the modal `open` state using a prop.
+
+```js
+import DashboardModal from 'uppy/lib/react/DashboardModal';
+```
+
+<!-- Make sure the old name of this section still works -->
+<a id="Options"></a>
+## Props
+
+On top of all the [Dashboard][] options, the `<DashboardModal />` plugin adds two additional props:
+
+ - `open` - Boolean true or false, setting this to `true` opens the modal and setting it to `false` closes it.
+ - `onRequestClose` - Callback called when the user attempts to close the modal, either by clicking the close button or by clicking outside the modal (if the `closeModalOnClickOutside` prop is set).
+
+To use other plugins like [Webcam][] with the `<DashboardModal />` component, add them to the Uppy instance and then specify their `id` in the [`plugins`](/docs/dashboard/#plugins) prop:
+
+```js
+// Do this wherever you initialize Uppy, eg. in a React component's constructor method.
+// Do NOT do it in `render()` or any other method that is called more than once!
+uppy.use(Webcam) // `id` defaults to "Webcam"
+uppy.use(Webcam, { id: 'MyWebcam' }) // `id` is… "MyWebcam"
+```
+
+Then do `plugins={['Webcam']}`.
+
+A full example that uses a button to open the modal is shown below:
+
+```js
+class MusicUploadButton extends React.Component {
+  constructor (props) {
+    super(props)
+
+    this.state = {
+      modalOpen: false
+    }
+
+    this.handleOpen = this.handleOpen.bind(this)
+    this.handleClose = this.handleClose.bind(this)
+  }
+
+  handleOpen () {
+    this.setState({
+      modalOpen: true
+    })
+  }
+
+  handleClose () {
+    this.setState({
+      modalOpen: false
+    })
+  }
+
+  render () {
+    return (
+      <div>
+        <button onClick={this.handleOpen}>Upload some music</button>
+        <DashboardModal
+          uppy={this.props.uppy}
+          closeModalOnClickOutside
+          open={this.state.modalOpen}
+          onRequestClose={this.handleClose}
+          plugins={['Webcam']}
+        />
+      </div>
+    );
+  }
+}
+```
+
+[Dashboard]: /docs/dashboard/
+[Webcam]: /docs/webcam/

+ 16 - 51
website/src/docs/react-dashboard.md

@@ -1,20 +1,21 @@
 ---
-title: "DashboardModal"
+title: "&lt;Dashboard />"
 type: docs
-permalink: docs/react/dashboard-modal/
-order: 51
+permalink: docs/react/dashboard/
+order: 64
 ---
 
-The `<DashboardModal />` component wraps the [Dashboard][] plugin, allowing control over the modal `open` state using a prop.
+The `<Dashboard />` component wraps the [Dashboard][] plugin. It only renders the Dashboard inline. To use the Dashboard modal (`inline: false`), use the [`<DashboardModal />`](/docs/react/dashboard-modal) component.
 
-## Options
+```js
+import Dashboard from 'uppy/lib/react/Dashboard';
+```
 
-On top of all the [Dashboard][] options, the `<DashboardModal />` plugin adds two additional props:
+## Props
 
- - `open` - Boolean true or false, setting this to `true` opens the modal and setting it to `false` closes it.
- - `onRequestClose` - Callback called when the user attempts to close the modal, either by clicking the close button or by clicking outside the modal (if the `closeModalOnClickOutside` prop is set).
+The `<Dashboard />` component supports all [Dashboard][] options as props.
 
-To use other plugins like [Webcam][] with the `<DashboardModal />` component, add them to the Uppy instance and then specify their `id` in the [`plugins`](/docs/dashboard/#plugins) prop:
+The `<Dashboard />` cannot be passed to a `target:` option of a remote provider or plugins like [Webcam][]. To use other plugins like [Webcam][] with the `<Dashboard />` component, first add them to the Uppy instance, and then specify their `id` in the [`plugins`](/docs/dashboard/#plugins) prop:
 
 ```js
 // Do this wherever you initialize Uppy, eg. in a React component's constructor method.
@@ -23,50 +24,14 @@ uppy.use(Webcam) // `id` defaults to "Webcam"
 uppy.use(Webcam, { id: 'MyWebcam' }) // `id` is… "MyWebcam"
 ```
 
-Then do `plugins={['Webcam']}`.
-
-A full example that uses a button to open the modal is shown below:
+Then in `render()` do:
 
 ```js
-class MusicUploadButton extends React.Component {
-  constructor (props) {
-    super(props)
-
-    this.state = {
-      modalOpen: false
-    }
-
-    this.handleOpen = this.handleOpen.bind(this)
-    this.handleClose = this.handleClose.bind(this)
-  }
-
-  handleOpen () {
-    this.setState({
-      modalOpen: true
-    })
-  }
-
-  handleClose () {
-    this.setState({
-      modalOpen: false
-    })
-  }
-
-  render () {
-    return (
-      <div>
-        <button onClick={this.handleOpen}>Upload some music</button>
-        <DashboardModal
-          uppy={this.props.uppy}
-          closeModalOnClickOutside
-          open={this.state.modalOpen}
-          onRequestClose={this.handleClose}
-          plugins={['Webcam']}
-        />
-      </div>
-    );
-  }
-}
+<Dashboard
+  plugins={['Webcam']}
+  {...props}
+/>
 ```
 
 [Dashboard]: /docs/dashboard/
+[Webcam]: /docs/webcam/

+ 26 - 0
website/src/docs/react-dragdrop.md

@@ -0,0 +1,26 @@
+---
+title: "&lt;DragDrop />"
+type: docs
+permalink: docs/react/dragdrop/
+order: 62
+---
+
+The `<DragDrop />` component wraps the [DragDrop][] plugin.
+
+```js
+import DragDrop from 'uppy/lib/react/DragDrop';
+```
+
+## Props
+
+The `<DragDrop />` component supports all [DragDrop][] options as props.
+
+```js
+<DragDrop
+  width="100%"
+  height="100%"
+  note="Images up to 200×200px"
+/>
+```
+
+[DragDrop]: /docs/dragdrop/

+ 25 - 0
website/src/docs/react-progressbar.md

@@ -0,0 +1,25 @@
+---
+title: "&lt;ProgressBar />"
+type: docs
+permalink: docs/react/progressbar/
+order: 63
+---
+
+The `<ProgressBar />` component wraps the [ProgressBar][] plugin.
+
+```js
+import ProgressBar from 'uppy/lib/react/ProgressBar';
+```
+
+## Props
+
+The `<ProgressBar />` component supports all [ProgressBar][] options as props.
+
+```js
+<ProgressBar
+  fixed
+  hideAfterFinish
+/>
+```
+
+[ProgressBar]: /docs/progressbar/

+ 26 - 0
website/src/docs/react-statusbar.md

@@ -0,0 +1,26 @@
+---
+title: "&lt;StatusBar />"
+type: docs
+permalink: docs/react/statusbar/
+order: 61
+---
+
+The `<StatusBar />` component wraps the [StatusBar][] plugin.
+
+```js
+import StatusBar from 'uppy/lib/react/StatusBar';
+```
+
+## Props
+
+The `<StatusBar />` component supports all [StatusBar][] options as props.
+
+```js
+<StatusBar
+  hideUploadButton
+  hideAfterFinish={false}
+  showProgressDetails
+/>
+```
+
+[StatusBar]: /docs/statusbar/

+ 11 - 9
website/src/docs/react.md

@@ -2,7 +2,7 @@
 title: "Introduction"
 type: docs
 permalink: docs/react/
-order: 50
+order: 60
 ---
 
 Uppy provides [React][] components for the included UI plugins.
@@ -35,8 +35,6 @@ uppy.on('complete', (result) => {
   })
 })
 
-uppy.run()
-
 const AvatarPicker = ({ currentAvatar }) => {
   return (
     <div>
@@ -56,16 +54,20 @@ const AvatarPicker = ({ currentAvatar }) => {
 
 The plugins that are available as React component wrappers are:
 
- - [Dashboard][]
- - [DashboardModal][]
- - [DragDrop][]
- - [ProgressBar][]
- - [StatusBar][]
+ - [&lt;Dashboard />][] - renders an inline [Dashboard][]
+ - [&lt;DashboardModal />][] - renders a [Dashboard][] modal
+ - [&lt;DragDrop />][] - renders a [DragDrop][] area
+ - [&lt;ProgressBar />][] - renders a [ProgressBar][]
+ - [&lt;StatusBar />][] - renders a [StatusBar][]
 
 [React]: https://facebook.github.io/react
 [Preact]: https://preactjs.com/
+[&lt;Dashboard />]: /docs/react/dashboard
+[&lt;DragDrop />]: /docs/react/dragdrop
+[&lt;ProgressBar />]: /docs/react/progressbar
+[&lt;StatusBar />]: /docs/react/statusbar
+[&lt;DashboardModal />]: /docs/react/dashboard-modal
 [Dashboard]: /docs/dashboard
 [DragDrop]: /docs/dragdrop
 [ProgressBar]: /docs/progressbar
 [StatusBar]: /docs/statusbar
-[DashboardModal]: /docs/react/dashboard-modal

+ 4 - 4
website/src/docs/redux.md

@@ -2,7 +2,7 @@
 title: "Redux"
 type: docs
 permalink: docs/redux
-order: 57
+order: 67
 ---
 
 Uppy supports popular [Redux](https://redux.js.org/) state management library in two ways:
@@ -41,10 +41,10 @@ const uppy = Uppy({
   meta: {
     username: 'John',
     license: 'Creative Commons'
+  }
 })
-.use(XHRUpload, { endpoint: 'https://example.com' })
-.use(ReduxDevTools)
-.run()
+  .use(XHRUpload, { endpoint: 'https://example.com' })
+  .use(ReduxDevTools)
 ```
 
 After you `.use(ReduxDevTools)`, you should be able to see Uppy’s state in Redux Dev Tools.

+ 71 - 30
website/src/docs/statusbar.md

@@ -8,6 +8,14 @@ permalink: docs/statusbar/
 The StatusBar shows upload progress and speed, ETAs, pre- and post-processing information, and allows users to control (pause/resume/cancel) the upload.
 Best used together with a simple file source plugin, such as [FileInput][] or [DragDrop][], or a custom implementation.
 
+```js
+const StatusBar = require('uppy/lib/plugins/StatusBar')
+
+uppy.use(StatusBar, {
+  // Options
+})
+```
+
 [Try it live](/examples/statusbar/)
 
 ## Options
@@ -18,39 +26,14 @@ uppy.use(StatusBar, {
   hideUploadButton: false,
   showProgressDetails: false,
   hideAfterFinish: true
-  locale: {
-    strings: {
-      uploading: 'Uploading',
-      complete: 'Complete',
-      uploadFailed: 'Upload failed',
-      pleasePressRetry: 'Please press Retry to upload again',
-      paused: 'Paused',
-      error: 'Error',
-      retry: 'Retry',
-      pressToRetry: 'Press to retry',
-      retryUpload: 'Retry upload',
-      resumeUpload: 'Resume upload',
-      cancelUpload: 'Cancel upload',
-      pauseUpload: 'Pause upload',
-      filesUploadedOfTotal: {
-        0: '%{complete} of %{smart_count} file uploaded',
-        1: '%{complete} of %{smart_count} files uploaded'
-      },
-      dataUploadedOfTotal: '%{complete} of %{total}',
-      xTimeLeft: '%{time} left',
-      uploadXFiles: {
-        0: 'Upload %{smart_count} file',
-        1: 'Upload %{smart_count} files'
-      },
-      uploadXNewFiles: {
-        0: 'Upload +%{smart_count} file',
-        1: 'Upload +%{smart_count} files'
-      }
-    }
-  }
+  locale: {}
 })
 ```
 
+### `id: 'StatusBar'`
+
+A unique identifier for this StatusBar. Defaults to `'StatusBar'`. Use this if you need to add multiple StatusBar instances.
+
 ### `target: null`
 
 DOM element, CSS selector, or plugin to mount the StatusBar into.
@@ -66,5 +49,63 @@ By default, progress in StatusBar is shown as simple percentage. If you’d like
 `showProgressDetails: false`: Uploading: 45%
 `showProgressDetails: true`: Uploading: 45%・43 MB of 101 MB・8s left
 
+### `locale: {}`
+
+Localize text that is shown to the user.
+
+The default English strings are:
+
+```js
+strings: {
+  // Shown in the status bar while files are being uploaded.
+  uploading: 'Uploading',
+  // Shown in the status bar once all files have been uploaded.
+  complete: 'Complete',
+  // Shown in the status bar if an upload failed.
+  uploadFailed: 'Upload failed',
+  // Shown next to `uploadFailed`.
+  pleasePressRetry: 'Please press Retry to upload again',
+  // Shown in the status bar while the upload is paused.
+  paused: 'Paused',
+  error: 'Error',
+  // Used as the label for the button that retries an upload.
+  retry: 'Retry',
+  // Used as the label for the button that cancels an upload.
+  cancel: 'Cancel',
+  // Used as the screen reader label for the button that retries an upload.
+  retryUpload: 'Retry upload',
+  // Used as the screen reader label for the button that pauses an upload.
+  pauseUpload: 'Pause upload',
+  // Used as the screen reader label for the button that resumes a paused upload.
+  resumeUpload: 'Resume upload',
+  // Used as the screen reader label for the button that cancels an upload.
+  cancelUpload: 'Cancel upload',
+  // When `showProgressDetails` is set, shows the number of files that have been fully uploaded so far.
+  filesUploadedOfTotal: {
+    0: '%{complete} of %{smart_count} file uploaded',
+    1: '%{complete} of %{smart_count} files uploaded'
+  },
+  // When `showProgressDetails` is set, shows the amount of bytes that have been uploaded so far.
+  dataUploadedOfTotal: '%{complete} of %{total}',
+  // When `showProgressDetails` is set, shows an estimation of how long the upload will take to complete.
+  xTimeLeft: '%{time} left',
+  // Used as the label for the button that starts an upload.
+  uploadXFiles: {
+    0: 'Upload %{smart_count} file',
+    1: 'Upload %{smart_count} files'
+  },
+  // Used as the label for the button that starts an upload, if another upload has been started in the past
+  // and new files were added later.
+  uploadXNewFiles: {
+    0: 'Upload +%{smart_count} file',
+    1: 'Upload +%{smart_count} files'
+  }
+}
+```
+
+### `replaceTargetContent: false`
+
+Remove all children of the `target` element before mounting the StatusBar. By default, Uppy will append any UI to the `target` DOM element. This is the least dangerous option. However, you may have some fallback HTML inside the `target` element in case JavaScript or Uppy is not available. In that case you can set `replaceTargetContent: true` to clear the `target` before appending.
+
 [FileInput]: https://github.com/transloadit/uppy/blob/master/src/plugins/FileInput.js
 [DragDrop]: /docs/dragdrop

+ 48 - 1
website/src/docs/transloadit.md

@@ -1,6 +1,6 @@
 ---
 type: docs
-order: 33
+order: 34
 title: "Transloadit"
 permalink: docs/transloadit/
 ---
@@ -10,6 +10,8 @@ The Transloadit plugin can be used to upload files to [Transloadit](https://tran
 [Try it live](/examples/transloadit/)
 
 ```js
+const Transloadit = require('uppy/lib/plugins/Transloadit')
+
 uppy.use(Transloadit, {
   service: 'https://api2.transloadit.com',
   params: null,
@@ -24,8 +26,35 @@ uppy.use(Transloadit, {
 
 As of Uppy 0.24 the Transloadit plugin includes the [Tus](/docs/tus) plugin to handle the uploading, so you no longer have to add it manually.
 
+## Properties
+
+### `Transloadit.UPPY_SERVER`
+
+The main endpoint for Transloadit's hosted uppy-servers. You can use this constant in remote provider options, like so:
+
+```js
+const Dropbox = require('uppy/lib/plugins/Dropbox')
+const Transloadit = require('uppy/lib/plugins/Transloadit')
+
+uppy.use(Dropbox, {
+  host: Transloadit.UPPY_SERVER
+})
+```
+
+The value of this constant is `https://api2.transloadit.com/uppy-server`. If you are using a custom [`service`](#service) option, you should also set a custom host option in your provider plugins, by taking a Transloadit API url and appending `/uppy-server`:
+
+```js
+uppy.use(Dropbox, {
+  host: 'https://api2-us-east-1.transloadit.com/uppy-server'
+})
+```
+
 ## Options
 
+### `id: 'Transloadit'`
+
+A unique identifier for this plugin. Defaults to `'Transloadit'`.
+
 ### `service`
 
 The Transloadit API URL to use. Defaults to `https://api2.transloadit.com`, which will attempt to route traffic efficiently based on where your users are. You can set this to something like `https://api2-us-east-1.transloadit.com` if you want to use a particular region.
@@ -182,6 +211,24 @@ uppy.use(Transloadit, {
 })
 ```
 
+### `locale: {}`
+
+Localize text that is shown to the user.
+
+The default English strings are:
+
+```js
+strings: {
+  // Shown while Assemblies are being created for an upload.
+  creatingAssembly: 'Preparing upload...'
+  // Shown if an Assembly could not be created.
+  creatingAssemblyFailed: 'Transloadit: Could not create Assembly',
+  // Shown after uploads have succeeded, but when the Assembly is still executing.
+  // This only shows if `waitForMetadata` or `waitForEncoding` was set.
+  encoding: 'Encoding...'
+}
+```
+
 ## Events
 
 ### `transloadit:assembly-created`

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

@@ -8,6 +8,8 @@ permalink: docs/tus/
 The Tus plugin brings [tus.io](http://tus.io) resumable file uploading to Uppy by wrapping the [tus-js-client][].
 
 ```js
+const Tus = require('uppy/lib/plugins/Tus')
+
 uppy.use(Tus, {
   endpoint: 'https://master.tus.io/files/', // use your tus endpoint here
   resume: true,
@@ -20,6 +22,10 @@ uppy.use(Tus, {
 
 The Tus plugin supports all of [tus-js-client][]’s options. Additionally:
 
+### `id: 'Tus'`
+
+A unique identifier for this plugin. Defaults to `'Tus'`.
+
 ### `resume: true`
 
 A boolean indicating whether tus should attempt to resume the upload if the upload has been started in the past. This includes storing the file’s upload url. Use false to force an entire reupload.
@@ -36,4 +42,8 @@ URL to upload to, where your tus.io server is running.
 
 Whether to auto-retry the upload when the user's internet connection is back online after an outage.
 
+### `limit: 0`
+
+Limit the amount of uploads going on at the same time. Passing `0` means no limit.
+
 [tus-js-client]: https://github.com/tus/tus-js-client

+ 19 - 13
website/src/docs/uppy.md

@@ -7,6 +7,12 @@ permalink: docs/uppy/
 
 Core module that orchestrates everything in Uppy, exposing `state`, `events` and `methods`.
 
+```js
+const Uppy = require('uppy/lib/core')
+
+const uppy = Uppy()
+```
+
 ## Options
 
 ```js
@@ -15,10 +21,10 @@ const uppy = Uppy({
   autoProceed: true,
   debug: false,
   restrictions: {
-    maxFileSize: false,
-    maxNumberOfFiles: false,
-    minNumberOfFiles: false,
-    allowedFileTypes: false
+    maxFileSize: null,
+    maxNumberOfFiles: null,
+    minNumberOfFiles: null,
+    allowedFileTypes: null
   },
   meta: {},
   onBeforeFileAdded: (currentFile, files) => currentFile,
@@ -52,10 +58,14 @@ Optionally provide rules and conditions for which files can be selected.
 
 **Parameters**
 
-- `maxFileSize` *number*
-- `maxNumberOfFiles` *number*
-- `minNumberOfFiles` *number*
-- `allowedFileTypes` *array* of wildcards or exact mime types, like `image/*`
+- `maxFileSize` *null | number*
+- `maxNumberOfFiles` *null | number*
+- `minNumberOfFiles` *null | number*
+- `allowedFileTypes` *null | array* of wildcards `image/*`, exact mime types `image/jpeg`, or file extensions `.jpg`: `['image/*', '.jpg', '.jpeg', '.png', '.gif']`
+
+`maxNumberOfFiles` affects the number of files user is able to select via the system file dialog in UI plugins like `DragDrop`, `FileInput` and `Dashboard`: when set to `1` they will only be able to select a single file, otherwise, when `null` or other number, they will be able to select multiple files.
+
+`allowedFileTypes` gets passed to the system file dialog via [`<input>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/file#Limiting_accepted_file_types)’s accept attribute, so only files matching these types will be selectable.
 
 ### `meta: {}`
 
@@ -69,7 +79,7 @@ meta: {
 
 This global metadata is added to each file in Uppy. It can be modified with two methods:
 
-1. [`uppy.setMeta({ username: 'Peter' })`](/docs/uppy/#uppy-setmeta-data) — set or update meta for all files.
+1. [`uppy.setMeta({ username: 'Peter' })`](/docs/uppy/#uppy-setMeta-data) — set or update meta for all files.
 2. [`uppy.setFileMeta('myfileID', { resize: 1500 })`](/docs/uppy/#uppy-setFileMeta-fileID-data) — set or update meta for specific file.
 
 Metadata from each file is then attached to uploads in [Tus](/docs/tus/) and [XHRUpload](/docs/xhrupload/) plugins.
@@ -212,10 +222,6 @@ const uppy = Uppy()
 uppy.use(DragDrop, { target: 'body' })
 ```
 
-### `uppy.run()`
-
-Initializes everything after setup. Must be called before calling `.upload()` or using Uppy in any meaningful way.
-
 ### `uppy.getID()`
 
 Get the uppy instance ID, see the [`id` option](#id-39-uppy-39).

+ 62 - 0
website/src/docs/url.md

@@ -0,0 +1,62 @@
+---
+type: docs
+order: 54
+title: "Url"
+permalink: docs/url/
+---
+
+The Url plugin lets users import files from the Internet. Paste any URL and it'll be added!
+
+An Uppy Server instance is required for the Url plugin to work. Uppy Server will download the files and upload them to their destination. This saves bandwidth for the user (especially on mobile connections) and helps avoid CORS restrictions.
+
+```js
+const Url = require('uppy/lib/plugins/Url')
+
+uppy.use(Url, {
+  // Options
+})
+```
+
+[Try live!](/examples/dashboard/)
+
+## Options
+
+```js
+uppy.use(Url, {
+  target: Dashboard,
+  host: 'https://server.uppy.io/',
+  locale: {}
+})
+```
+
+### `id: 'Url'`
+
+A unique identifier for this plugin. Defaults to `'Url'`.
+
+### `target: null`
+
+DOM element, CSS selector, or plugin to mount the Url provider into. This should normally be the Dashboard.
+
+### `host: null`
+
+URL to an Uppy Server instance.
+
+### `locale: {}`
+
+Localize text that is shown to the user.
+
+The default English strings are:
+
+```js
+strings: {
+  // Label for the "Import" button.
+  import: 'Import',
+  // Placeholder text for the URL input.
+  enterUrlToImport: 'Enter URL to import a file',
+  // Error message shown if Uppy Server could not load a URL.
+  failedToFetch: 'Uppy Server failed to fetch this URL, please make sure it’s correct',
+  // Error message shown if the input does not look like a URL.
+  enterCorrectUrl: 'Incorrect URL: Please make sure you are entering a direct link to a file'
+}
+```
+

+ 32 - 6
website/src/docs/webcam.md

@@ -9,6 +9,14 @@ The Webcam plugin lets you take photos and record videos with a built-in camera
 
 > To use the Webcam plugin in Chrome, [your site should be served over https](https://developers.google.com/web/updates/2015/10/chrome-47-webrtc#public_service_announcements). This restriction does not apply on `localhost`, so you don't have to jump through many hoops during development.
 
+```js
+const Webcam = require('uppy/lib/plugins/Webcam')
+
+uppy.use(Webcam, {
+  // Options
+})
+```
+
 [Try live!](/examples/dashboard/)
 
 ## Options
@@ -25,14 +33,14 @@ uppy.use(Webcam, {
   ],
   mirror: true,
   facingMode: 'user',
-  locale: {
-    strings: {
-      smile: 'Smile!'
-    }
-  }
+  locale: {}
 })
 ```
 
+### `id: 'Webcam'`
+
+A unique identifier for this plugin. Defaults to `'Webcam'`.
+
 ### `target: null`
 
 DOM element, CSS selector, or plugin to mount Webcam into.
@@ -72,4 +80,22 @@ Devices sometimes have multiple cameras, front and back, for example. There’s
 
 ### `locale: {}`
 
-There is only one localizable string: `strings.smile`. It's shown before a picture is taken, when the `countdown` option is set to true.
+Localize text that is shown to the user.
+
+The default English strings are:
+
+```js
+strings: {
+  // Shown before a picture is taken when the `countdown` option is set.
+  smile: 'Smile!',
+  // Used as the label for the button that takes a picture.
+  // This is not visibly rendered but is picked up by screen readers.
+  takePicture: 'Take a picture',
+  // Used as the label for the button that starts a video recording.
+  // This is not visibly rendered but is picked up by screen readers.
+  startRecording: 'Begin video recording',
+  // Used as the label for the button that stops a video recording.
+  // This is not visibly rendered but is picked up by screen readers.
+  stopRecording: 'Stop video recording',
+}
+```

+ 1 - 1
website/src/docs/writing-plugins.md

@@ -2,7 +2,7 @@
 type: docs
 title: "Writing Plugins"
 permalink: docs/writing-plugins/
-order: 20
+order: 11
 ---
 
 There are a few useful Uppy plugins out there, but there might come a time when you’ll want to build your own.

+ 26 - 4
website/src/docs/xhrupload.md

@@ -7,16 +7,22 @@ permalink: docs/xhrupload/
 
 The XHRUpload plugin handles classic HTML multipart form uploads, as well as uploads using the HTTP `PUT` method.
 
-[Try it live](/examples/xhrupload/)
-
 ```js
+const XHRUpload = require('uppy/lib/plugins/XHRUpload')
+
 uppy.use(XHRUpload, {
   endpoint: 'http://my-website.org/upload'
 })
 ```
 
+[Try it live](/examples/xhrupload/)
+
 ## Options
 
+### `id: 'XHRUpload'`
+
+A unique identifier for this plugin. Defaults to `'XHRUpload'`.
+
 ### `endpoint: ''`
 
 URL to upload to.
@@ -147,6 +153,19 @@ The default is 30 seconds.
 
 Limit the amount of uploads going on at the same time. Passing `0` means no limit.
 
+### `locale: {}`
+
+Localize text that is shown to the user.
+
+The default English strings are:
+
+```js
+strings: {
+  // Shown in the Informer if an upload is being canceled because it stalled for too long.
+  timedOut: 'Upload stalled for %{seconds} seconds, aborting.'
+}
+```
+
 ## POST Parameters / Form Fields
 
 When using XHRUpload with `formData: true`, file metadata is sent along with each upload request. You can set metadata for a file using [`uppy.setFileMeta(fileID, data)`](/docs/uppy#uppy-setFileMeta-fileID-data), or for all files simultaneously using [`uppy.setMeta(data)`](/docs/uppy#uppy-setMeta-data).
@@ -183,9 +202,12 @@ The default form field for file uploads is `files[]`, which means you have to ac
 // upload.php
 $files = $_FILES['files'];
 $file_path = $files['tmp_name'][0]; // temporary upload path of the first file
-move_uploaded_file($file_path, './img/img.png'); // save the file at `img/img.png`
+$file_name = $_POST['name']; // desired name of the file
+move_uploaded_file($file_path, './img/' . basename($file_name)); // save the file in `img/`
 ```
 
+Note how we're using `$_POST['name']` instead of `$my_file['name']`. `$my_file['name']` contains the original name of the file on the user's device. `$_POST['name']` contains the `name` metadata value for the uploaded file, which can be edited by the user using the [Dashboard](/docs/dashboard).
+
 Set a custom `fieldName` to make working with the `$_FILES` array a bit less convoluted:
 
 ```js
@@ -201,7 +223,7 @@ uppy.use(XHRUpload, {
 // upload.php
 $my_file = $_FILES['my_file'];
 $file_path = $my_file['tmp_name']; // temporary upload path of the file
-$file_name = $my_file['name']; // original name of the file
+$file_name = $_POST['name']; // desired name of the file
 move_uploaded_file($file_path, './img/' . basename($file_name)); // save the file at `img/FILE_NAME`
 ```
 

+ 0 - 1
website/src/examples/bundle/app.html

@@ -8,7 +8,6 @@
   uppy.use(Uppy.Tus, {endpoint: '//master.tus.io/files/'});
   uppy.use(Uppy.DragDrop, {target: '#uppy-dnd'});
   uppy.use(Uppy.ProgressBar);
-  uppy.run();
 
   console.log('Uppy loaded from CDN with tus enabled');
 </script>

+ 0 - 1
website/src/examples/dashboard/app.es6

@@ -69,7 +69,6 @@ function uppyInit () {
   }
 
   uppy.use(Tus, { endpoint: 'https://master.tus.io/files/', resume: true })
-  uppy.run()
 
   uppy.on('complete', result => {
     console.log('successful files:')

+ 0 - 1
website/src/examples/dashboard/index.ejs

@@ -55,7 +55,6 @@ const uppy = Uppy({
 .use(Instagram, { target: Dashboard, host: 'https://server.uppy.io' })
 .use(Webcam, { target: Dashboard })
 .use(Tus, { endpoint: 'https://master.tus.io/files/' })
-.run()
 
 uppy.on('complete', result => {
   console.log('successful files:', result.successful)

+ 0 - 2
website/src/examples/dragdrop/app.es6

@@ -8,14 +8,12 @@ uppyOne
   .use(DragDrop, {target: '.UppyDragDrop-One'})
   .use(Tus, {endpoint: '//master.tus.io/files/'})
   .use(ProgressBar, {target: '.UppyDragDrop-One-Progress', hideAfterFinish: false})
-  .run()
 
 const uppyTwo = new Uppy({debug: true, autoProceed: false})
 uppyTwo
   .use(DragDrop, {target: '#UppyDragDrop-Two'})
   .use(Tus, {endpoint: '//master.tus.io/files/'})
   .use(ProgressBar, {target: '.UppyDragDrop-Two-Progress', hideAfterFinish: false})
-  .run()
 
 var uploadBtn = document.querySelector('.UppyDragDrop-Two-Upload')
 uploadBtn.addEventListener('click', function () {

+ 2 - 3
website/src/examples/i18n/app.html

@@ -1,11 +1,11 @@
 <!-- Basic Uppy styles. You can use Transloadit's CDN, Edgly: 
-https://transloadit.edgly.net/releases/uppy/v0.24.3/dist/uppy.min.css -->
+https://transloadit.edgly.net/releases/uppy/v0.24.4/dist/uppy.min.css -->
 <link rel="stylesheet" href="/uppy/uppy.min.css">
 
 <div class="UppyDragDrop"></div>
 
 <!-- Load Uppy pre-built bundled version. You can use Transloadit's CDN, Edgly: 
-https://transloadit.edgly.net/releases/uppy/v0.24.3/dist/uppy.min.js -->
+https://transloadit.edgly.net/releases/uppy/v0.24.4/dist/uppy.min.js -->
 <script src="/uppy/uppy.min.js"></script>
 <script>
   var uppy = Uppy.Core({ debug: true });
@@ -20,7 +20,6 @@ https://transloadit.edgly.net/releases/uppy/v0.24.3/dist/uppy.min.js -->
   });
   uppy.use(Uppy.ProgressBar, { target: 'body', fixed: true, hideAfterFinish: false });
   uppy.use(Uppy.Tus, { endpoint: '//master.tus.io/files/' });
-  uppy.run();
 
   console.log('--> Uppy pre-built version with Tus, DragDrop & Russian language pack has loaded');
 </script>

+ 0 - 1
website/src/examples/statusbar/app.es6

@@ -8,4 +8,3 @@ uppyOne
   .use(FileInput, { target: '.UppyInput', pretty: false })
   .use(Tus, { endpoint: '//master.tus.io/files/' })
   .use(StatusBar, { target: '.UppyInput-Progress', hideUploadButton: true })
-  .run()

+ 0 - 1
website/src/examples/transloadit/app.es6

@@ -59,7 +59,6 @@ function initUppy () {
     })
     .use(Instagram, { target: Dashboard, host: 'https://api2.transloadit.com/uppy-server' })
     .use(Webcam, { target: Dashboard })
-    .run()
 
   uppy
     .on('transloadit:result', (stepName, result) => {

+ 1 - 2
website/src/examples/transloadit/index.ejs

@@ -37,7 +37,7 @@ This example demonstrates how to unlock Transloadit’s features within Uppy.
     Transloadit API Key:</label>
   <input type="text"
          style="font-size: 15px; width: 300px; max-width: 100%; border: 0; border-bottom: 1px solid black; padding: 6px 8px; margin-bottom: 20px;"
-         id="transloadit-api-key" 
+         id="transloadit-api-key"
          placeholder="Your Transloadit API Key">
 </p>
 
@@ -147,5 +147,4 @@ uppy
       .getElementById('uppy-transloadit-result')
       .appendChild(resultContainer)
   })
-  .run()
 {% endcodeblock %}

+ 0 - 1
website/src/examples/xhrupload/app.es6

@@ -15,6 +15,5 @@ uppy.use(ProgressBar, {
   fixed: true,
   hideAfterFinish: false
 })
-uppy.run()
 
 console.log('Uppy with Formtag and XHRUpload is loaded')

+ 0 - 1
website/src/uppy.ejs

@@ -21,7 +21,6 @@ permalink: uppy/
       // uppy.use(Uppy.plugins.DragDrop, {target: 'body'});
         .use(Uppy.plugins.Modal, {trigger: '#uppyModalOpener'})
         .use(Uppy.plugins.Dummy, {target: Uppy.plugins.Modal})
-        .run();
     </script>
   </body>
 </html>

+ 4 - 5
website/themes/uppy/layout/index.ejs

@@ -79,15 +79,15 @@
   <p>© <%- date(Date.now(), 'YYYY') %> <a href="https://transloadit.com" rel="noreferrer noopener" target="_blank">Transloadit</a></p>
 </footer>
 
-<link href="https://transloadit.edgly.net/releases/uppy/v0.24.3/dist/uppy.min.css" rel="stylesheet">
-<script src="https://transloadit.edgly.net/releases/uppy/v0.24.3/dist/uppy.min.js"></script>
+<link href="https://transloadit.edgly.net/releases/uppy/v0.24.4/dist/uppy.min.css" rel="stylesheet">
+<script src="https://transloadit.edgly.net/releases/uppy/v0.24.4/dist/uppy.min.js"></script>
 
 <script>
   var PROTOCOL = location.protocol === 'https:' ? 'https' : 'http'
   var TUS_ENDPOINT = PROTOCOL + '://master.tus.io/files/'
-  
+
   var uppy = Uppy.Core({ debug: true, autoProceed: false })
-  .use(Uppy.Dashboard, { 
+  .use(Uppy.Dashboard, {
     trigger: '#select-files',
     target: '#upload-form',
     replaceTargetContent: true,
@@ -97,7 +97,6 @@
     ]
   })
   .use(Uppy.Tus, { endpoint: TUS_ENDPOINT})
-  .run()
 
   uppy.on('success', (files) => {
     console.log(`Upload complete! We’ve uploaded these files: ${files}`)

+ 7 - 0
website/themes/uppy/layout/partials/sidebar.ejs

@@ -19,6 +19,13 @@
           <li>
             <a href="/<%- path %>" class="sidebar-link<%- page.title === p.title ? ' current' : '' %><%- p.is_new ? ' new' : '' %>"><%- p.title %></a>
           </li>
+        <% } else if (path === 'docs/providers/') { %>
+          <li>
+            <h3><a href="/docs/providers/">Remote Providers</a></h3>
+          </li>
+          <li>
+            <a href="/<%- path %>" class="sidebar-link<%- page.title === p.title ? ' current' : '' %><%- p.is_new ? ' new' : '' %>"><%- p.title %></a>
+          </li>
         <% } else if (path === 'docs/react/') { %>
           <li>
             <h3><a href="/docs/react/">React Components</a></h3>

+ 1 - 1
website/themes/uppy/layout/partials/social.ejs

@@ -6,7 +6,7 @@ allowtransparency="true" frameborder="0" scrolling="0" width="100" height="20"><
   <li><a href="https://community.transloadit.com/c/uppy"><img src="/images/community-forum-d8006a.svg" /> </a></li>
   <li class="BuildBadge">
    <span class="wrapper">
-     <a href="https://travis-ci.org/transloadit/uppy"><img src="https://travis-ci.org/transloadit/uppy.svg" alt="Build Status"></a>
+     <a href="https://travis-ci.org/transloadit/uppy"><img src="https://travis-ci.org/transloadit/uppy.svg?branch=master" alt="Build Status"></a>
    </span>
   </li>
   <li class="SauceBadge">

Nem az összes módosított fájl került megjelenítésre, mert túl sok fájl változott