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

Merge branch 'master' into feature/auto-run

Artur Paikin 7 éve
szülő
commit
0dfd1a2610
53 módosított fájl, 904 hozzáadás és 528 törlés
  1. 1 1
      .travis.yml
  2. 57 11
      CHANGELOG.md
  3. 3 3
      README.md
  4. 1 1
      bin/upload-to-cdn.sh
  5. 2 2
      examples/cdn-example/index.html
  6. 2 2
      examples/uppy-with-server/client/index.html
  7. 8 8
      package-lock.json
  8. 1 1
      package.json
  9. 26 12
      src/core/Core.js
  10. 46 2
      src/core/Core.test.js
  11. 28 4
      src/core/Plugin.js
  12. 0 3
      src/core/UppySocket.js
  13. 7 5
      src/plugins/Dashboard/ActionBrowseTagline.js
  14. 4 4
      src/plugins/Dashboard/Dashboard.js
  15. 3 5
      src/plugins/Dashboard/FileCard.js
  16. 3 1
      src/plugins/Dashboard/FileList.js
  17. 9 8
      src/plugins/Dashboard/Tabs.js
  18. 6 2
      src/plugins/Dashboard/index.js
  19. 24 6
      src/plugins/DragDrop/index.js
  20. 6 6
      src/plugins/Dropbox/icons.js
  21. 4 0
      src/plugins/Dropbox/index.js
  22. 11 4
      src/plugins/FileInput.js
  23. 12 0
      src/plugins/GoogleDrive/index.js
  24. 6 1
      src/plugins/Instagram/index.js
  25. 4 4
      src/plugins/StatusBar/StatusBar.js
  26. 1 0
      src/plugins/StatusBar/index.js
  27. 55 19
      src/plugins/Transloadit/index.js
  28. 52 0
      src/plugins/Transloadit/index.test.js
  29. 37 16
      src/plugins/Tus.js
  30. 4 7
      src/scss/_common.scss
  31. 15 9
      src/scss/_dashboard.scss
  32. 2 12
      src/scss/_dragdrop.scss
  33. 4 3
      src/scss/_provider.scss
  34. 2 2
      src/scss/_statusbar.scss
  35. 2 1
      src/scss/_webcam.scss
  36. 27 14
      src/views/ProviderView/AuthView.js
  37. 3 1
      src/views/ProviderView/Browser.js
  38. 2 2
      src/views/ProviderView/Item.js
  39. 3 11
      src/views/ProviderView/ItemList.js
  40. 4 1
      src/views/ProviderView/index.js
  41. 240 265
      website/package-lock.json
  42. 11 11
      website/package.json
  43. 76 15
      website/src/docs/aws-s3.md
  44. 15 1
      website/src/docs/dragdrop.md
  45. 5 8
      website/src/docs/fileinput.md
  46. 2 2
      website/src/docs/index.md
  47. 23 0
      website/src/docs/transloadit.md
  48. 4 0
      website/src/docs/tus.md
  49. 28 24
      website/src/docs/uppy.md
  50. 2 0
      website/src/docs/webcam.md
  51. 5 2
      website/src/docs/xhrupload.md
  52. 4 4
      website/src/examples/i18n/app.html
  53. 2 2
      website/themes/uppy/layout/index.ejs

+ 1 - 1
.travis.yml

@@ -1,6 +1,6 @@
 language: node_js
 node_js:
-- 6.11.3
+- 8.11.1
 addons:
   apt:
     sources:

+ 57 - 11
CHANGELOG.md

@@ -46,7 +46,6 @@ PRs are welcome! Please do open an issue to discuss first if it's a big feature,
 - [ ] Prepare for (piwik-) tracking of usage of uppy ? see #83
 - [ ] screenshot+screencast support similar to Webcam #148
 - [ ] Webcam modes #198
-- [ ] feature: improved UI for Provider, Google Drive and Instagram, grid/list views
 - [ ] feature: React Native support
 - [ ] consider iframe / more security for Transloadit/Uppy integration widget and Uppy itself. Page can’t get files from Google Drive if its an iframe; possibility for folder restriction for provider plugins
 - [ ] It would be nice in the long run to have a dynamic package builder here right on the website where you can select the plugins you need/want and it builds and downloads a minified version of them?
@@ -65,6 +64,10 @@ Sort of like jQuery UI: https://jqueryui.com/download/
 - [ ] provider: Add Box
 - [ ] provider: change ProviderViews signature to receive Provider instance in second param. ref https://github.com/transloadit/uppy/pull/743#discussion_r180106070
 - [ ] 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
 
@@ -72,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)
@@ -91,13 +95,17 @@ 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
 
-- [ ] dashboard: allow minimizing the Dashboard during upload (Uppy then becomes just a tiny progress indicator) (@arturi)
+To Be Released: 2018-05-31.
+
 - [ ] dashboard: cancel button for transloadit assemblies (@arturi, @goto-bus-stop)
-- [ ] dashboard: option for Boolean metadata #454 (@arturi)
 - [ ] core: figure out per-plugin locales and i18n strings packs #491
 - [ ] goldenretriever: confirmation before restore #443
 - [ ] goldenretriever: add “ghost” files (@arturi)
@@ -111,16 +119,46 @@ What we need to do to release Uppy 1.0
 - [ ] uppy-server: benchmarks / stress test, large file, uppy-server / tus / S3 (10 GB)
 - [ ] uppy-server: document docker image setup for uppy-server (@ifedapoolarewaju)
 - [ ] 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)
+- [ ] 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
 - [ ] core: look into utilizing https://github.com/que-etc/resize-observer-polyfill for responsive components. See also https://github.com/transloadit/uppy/issues/750
-- [x] statusbar: add some spacing between text elements (#760 / @goto-bus-stop)
-- [ ] 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/.
 - [x] docs: add note in docs or solve the .run() issue, see #756
-- [x] core: add `uppy.getFiles()` method (@goto-bus-stop / #770)
-- [x] dashboard: fix duplicate plugin IDs, see #702 (@goto-bus-stop)
-- [x] react: update propTypes (#776 / @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/ (@goto-bus-stop)
+- [ ] core: customizing metadata fields, boolean metadata; see #809, #454 and related (@arturi)
+- [ ] providers: Add user/account names to Uppy provider views (@ifedapoolarewaju)
+
+## 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
+
+Released: 2018-05-10.
+
+- core: add `uppy.getFiles()` method (@goto-bus-stop / #770)
+- core: merge meta data when add file (#810 / @mrbatista)
+- 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)
+- instagram: add extensions to instagram files (@ifedapoolarewaju)
+- 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
 
@@ -168,6 +206,14 @@ Released: 2018-04-12.
 - url: Add checks for protocols, assume `http` when no protocol is used (#682 / @arturi)
 - url: Refactor things into Provider, see comments in  https://github.com/transloadit/uppy/pull/588; exposing the Provider module and the ProviderView to the public API (#727 / @ifedapoolarewaju, @arturi)
 - webcam: Styles updates: adapt for mobile, better camera icon, move buttons to the bottom bar (#682 / @arturi)
+- server: Fixed security vulnerability in transient dependency [#70](https://github.com/transloadit/uppy-server/issues/70) (@ifedapoolarewaju)
+- server: Auto-generate tmp download file name to avoid Path traversal (@ifedapoolarewaju)
+- server: Namespace redis key storage/lookup to avoid collisions (@ifedapoolarewaju)
+- server: Validate callback redirect url after completing OAuth (@ifedapoolarewaju)
+- server: Reduce the permission level required by Google Drive (@ifedapoolarewaju)
+- server: Auto-generate Server secret if none is provided on startup (@ifedapoolarewaju)
+- server: We implemented a more standard logger for Uppy Server (@ifedapoolarewaju)
+- server: Added an example project to run Uppy Server on Serverless (@ifedapoolarewaju)
 
 ## 0.23.3
 

+ 3 - 3
README.md

@@ -63,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.2/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.
 
@@ -72,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.2/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.2/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:

+ 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.2/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:

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

@@ -4,11 +4,11 @@
     <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.2/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.2/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' })

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

@@ -4,11 +4,11 @@
     <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.2/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.2/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' })

+ 8 - 8
package-lock.json

@@ -1,6 +1,6 @@
 {
   "name": "uppy",
-  "version": "0.24.2",
+  "version": "0.24.4",
   "lockfileVersion": 1,
   "requires": true,
   "dependencies": {
@@ -6599,8 +6599,8 @@
           "dev": true,
           "optional": true,
           "requires": {
-            "co": "4.6.0",
-            "json-stable-stringify": "1.0.1"
+            "co": "^4.6.0",
+            "json-stable-stringify": "^1.0.1"
           }
         },
         "ansi-regex": {
@@ -7031,7 +7031,7 @@
           "dev": true,
           "optional": true,
           "requires": {
-            "jsonify": "0.0.0"
+            "jsonify": "~0.0.0"
           }
         },
         "json-stringify-safe": {
@@ -7131,8 +7131,8 @@
           "dev": true,
           "optional": true,
           "requires": {
-            "abbrev": "1.1.0",
-            "osenv": "0.1.4"
+            "abbrev": "1",
+            "osenv": "^0.1.4"
           }
         },
         "npmlog": {
@@ -7190,8 +7190,8 @@
           "dev": true,
           "optional": true,
           "requires": {
-            "os-homedir": "1.0.2",
-            "os-tmpdir": "1.0.2"
+            "os-homedir": "^1.0.0",
+            "os-tmpdir": "^1.0.0"
           }
         },
         "path-is-absolute": {

+ 1 - 1
package.json

@@ -1,6 +1,6 @@
 {
   "name": "uppy",
-  "version": "0.24.2",
+  "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",

+ 26 - 12
src/core/Core.js

@@ -33,7 +33,8 @@ class Uppy {
         uppyServerError: 'Connection with Uppy Server failed',
         failedToUpload: 'Failed to upload',
         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,
@@ -324,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) {
@@ -387,15 +400,16 @@ class Uppy {
 
     const fileID = Utils.generateFileID(file)
 
+    const meta = file.meta || {}
+    meta.name = fileName
+    meta.type = fileType
+
     const newFile = {
       source: file.source || '',
       id: fileID,
       name: fileName,
       extension: fileExtension || '',
-      meta: Object.assign({}, this.getState().meta, {
-        name: fileName,
-        type: fileType
-      }),
+      meta: Object.assign({}, this.getState().meta, meta),
       type: fileType,
       data: file.data,
       progress: {
@@ -1093,7 +1107,7 @@ class Uppy {
     // Not returning the `catch`ed promise, because we still want to return a rejected
     // promise from this method if the upload failed.
     lastStep.catch((err) => {
-      this.emit('error', err)
+      this.emit('error', err, uploadID)
 
       this._removeUpload(uploadID)
     })

+ 46 - 2
src/core/Core.test.js

@@ -843,6 +843,28 @@ describe('src/Core', () => {
         boo: 'moo'
       })
     })
+
+    it('should merge meta data when add file', () => {
+      const core = new Core({
+        meta: { foo2: 'bar2' }
+      })
+      core.addFile({
+        source: 'jest',
+        name: 'foo.jpg',
+        type: 'image/jpeg',
+        meta: {
+          resize: 5000
+        },
+        data: new File([sampleImage], { type: 'image/jpeg' })
+      })
+      const fileId = Object.keys(core.state.files)[0]
+      expect(core.state.files[fileId].meta).toEqual({
+        name: 'foo.jpg',
+        type: 'image/jpeg',
+        foo2: 'bar2',
+        resize: 5000
+      })
+    })
   })
 
   describe('progress', () => {
@@ -1012,7 +1034,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: {
@@ -1034,6 +1056,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,
@@ -1257,7 +1301,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
     }

+ 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)

+ 7 - 5
src/plugins/Dashboard/ActionBrowseTagline.js

@@ -11,8 +11,9 @@ class ActionBrowseTagline extends Component {
   }
 
   render () {
-    // empty value=""  on file input, so we can select same file
-    // after removing it from Uppy — otherwise OS thinks it’s selected
+    // 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
@@ -22,13 +23,14 @@ class ActionBrowseTagline extends Component {
           {this.props.i18n('browse')}
         </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}
           onchange={this.props.handleInputChange}
+          accept={this.props.allowedFileTypes}
           value=""
           ref={(input) => {
             this.input = input

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

@@ -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'}
@@ -57,7 +57,7 @@ module.exports = function Dashboard (props) {
           aria-label={props.i18n('closeModal')}
           title={props.i18n('closeModal')}
           onclick={props.closeModal}>
-          <span aria-hidden="true">×</span>
+          <span aria-hidden="true">&times;</span>
         </button>
 
         <div class="uppy-Dashboard-innerWrap">

+ 3 - 5
src/plugins/Dashboard/FileCard.js

@@ -60,7 +60,7 @@ module.exports = class FileCard extends Component {
 
     return <div class="uppy-DashboardFileCard" aria-hidden={!this.props.fileCardFor}>
       {this.props.fileCardFor &&
-        <div style="width: 100%; height: 100%;">
+        <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>
             <button class="uppy-DashboardContent-back" type="button" title={this.props.i18n('finishEditingFile')}
@@ -79,12 +79,10 @@ 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>

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

@@ -18,7 +18,9 @@ module.exports = (props) => {
           {h(ActionBrowseTagline, {
             acquirers: props.acquirers,
             handleInputChange: props.handleInputChange,
-            i18n: props.i18n
+            i18n: props.i18n,
+            allowedFileTypes: props.allowedFileTypes,
+            maxNumberOfFiles: props.maxNumberOfFiles
           })}
         </div>
         { props.note && <div class="uppy-Dashboard-note">{props.note}</div> }

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

@@ -29,27 +29,28 @@ class Tabs extends Component {
       )
     }
 
-    // empty value=""  on file input, so we can select same file
-    // after removing it from Uppy — otherwise OS thinks it’s selected
-
+    // 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 <div class="uppy-DashboardTabs">
       <ul class="uppy-DashboardTabs-list" role="tablist">
         <li class="uppy-DashboardTab" role="presentation">
           <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 +60,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)}>

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

@@ -54,6 +54,8 @@ module.exports = class Dashboard extends Plugin {
         editFile: 'Edit file',
         editing: 'Editing',
         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',
@@ -240,8 +242,8 @@ module.exports = class Dashboard extends Plugin {
       document.body.classList.add('uppy-Dashboard-isOpen')
     }
 
+    this.rerender()
     this.updateDashboardElWidth()
-    // this.setFocusToFirstNode()
     this.setFocusToBrowse()
   }
 
@@ -486,7 +488,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
     })
   }
 

+ 24 - 6
src/plugins/DragDrop/index.js

@@ -25,9 +25,10 @@ module.exports = class DragDrop extends Plugin {
     // Default options
     const defaultOpts = {
       target: null,
+      inputName: 'files[]',
       width: '100%',
       height: '100%',
-      note: '',
+      note: null,
       locale: defaultLocale
     }
 
@@ -102,11 +103,25 @@ module.exports = class DragDrop extends Plugin {
   }
 
   render (state) {
-    const DragDropClass = `uppy 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
+    // after removing — otherwise browser thinks it’s already selected
     return (
       <div class={DragDropClass} style={DragDropStyle}>
         <div class="uppy-DragDrop-inner">
@@ -114,14 +129,17 @@ 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="files[]"
-              multiple="true"
+              name={this.opts.inputName}
+              multiple={restrictions.maxNumberOfFiles !== 1}
+              accept={restrictions.allowedFileTypes}
               ref={(input) => {
                 this.input = input
               }}
-              onchange={this.handleInputChange} />
+              onchange={this.handleInputChange}
+              value="" />
             {this.i18n('dropHereOr')} <span class="uppy-DragDrop-dragText">{this.i18n('browse')}</span>
           </label>
           <span class="uppy-DragDrop-note">{this.opts.note}</span>

+ 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'
   }

+ 11 - 4
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,14 +69,21 @@ module.exports = class FileInput extends Plugin {
       zIndex: -1
     }
 
-    return <div class="uppy uppy-FileInput-container">
+    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
+    return <div class="uppy-Root uppy-FileInput-container">
       <input class="uppy-FileInput-input"
         style={this.opts.pretty && hiddenInputStyle}
         type="file"
         name={this.opts.inputName}
         onchange={this.handleInputChange}
-        multiple={this.opts.allowMultipleFiles}
-        ref={(input) => { this.input = input }} />
+        multiple={restrictions.maxNumberOfFiles !== 1}
+        accept={restrictions.allowedFileTypes}
+        ref={(input) => { this.input = input }}
+        value="" />
       {this.opts.pretty &&
         <button class="uppy-FileInput-btn" type="button" onclick={this.handleClick}>
           {this.i18n('chooseFiles')}

+ 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'
   }

+ 6 - 1
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
   }
@@ -102,6 +106,7 @@ module.exports = class Instagram extends Plugin {
 
   getItemName (item) {
     if (item && item['created_time']) {
+      const ext = item.type === 'video' ? 'mp4' : 'jpeg'
       let date = new Date(item['created_time'] * 1000)
       date = date.toLocaleDateString([], {
         year: 'numeric',
@@ -111,7 +116,7 @@ module.exports = class Instagram extends Plugin {
         minute: 'numeric'
       })
       // adding both date and carousel_id, so the name is unique
-      return `Instagram ${date} ${item.carousel_id || ''}`
+      return `Instagram ${date} ${item.carousel_id || ''}.${ext}`
     }
     return ''
   }

+ 4 - 4
src/plugins/StatusBar/StatusBar.js

@@ -141,7 +141,7 @@ const CancelBtn = (props) => {
   return <button type="button"
     class="uppy-u-reset uppy-c-btn uppy-StatusBar-actionBtn uppy-StatusBar-actionBtn--cancel"
     aria-label={props.i18n('cancel')}
-    onclick={props.cancelAll}>Cancel{props.i18n('cancel')}</button>
+    onclick={props.cancelAll}>{props.i18n('cancel')}</button>
 }
 
 const PauseResumeButtons = (props) => {
@@ -172,15 +172,15 @@ const ProgressBarProcessing = (props) => {
   const value = Math.round(props.value * 100)
 
   return <div class="uppy-StatusBar-content">
-    {props.mode === 'determinate' ? `${value}%` : ''}
+    {props.mode === 'determinate' ? `${value}% \u00B7 ` : ''}
     {props.message}
   </div>
 }
 
 const progressDetails = (props) => {
   return <span class="uppy-StatusBar-statusSecondary">
-    { props.inProgress > 1 && props.i18n('filesUploadedOfTotal', { complete: props.complete, smart_count: props.inProgress }) + '' }
-    { props.i18n('dataUploadedOfTotal', { complete: props.totalUploadedSize, total: props.totalSize }) }
+    { props.inProgress > 1 && props.i18n('filesUploadedOfTotal', { complete: props.complete, smart_count: props.inProgress }) + ' \u00B7 ' }
+    { props.i18n('dataUploadedOfTotal', { complete: props.totalUploadedSize, total: props.totalSize }) + ' \u00B7 ' }
     { props.i18n('xTimeLeft', { time: props.totalETA }) }
   </span>
 }

+ 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',

+ 55 - 19
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.
  */
@@ -53,6 +57,7 @@ module.exports = class Transloadit extends Plugin {
 
     this.prepareUpload = this.prepareUpload.bind(this)
     this.afterUpload = this.afterUpload.bind(this)
+    this.handleError = this.handleError.bind(this)
     this.onFileUploadURLAvailable = this.onFileUploadURLAvailable.bind(this)
     this.onRestored = this.onRestored.bind(this)
     this.getPersistentData = this.getPersistentData.bind(this)
@@ -180,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}`
@@ -227,7 +228,7 @@ module.exports = class Transloadit extends Plugin {
       return this.connectSocket(assembly)
         .then(() => assembly)
     }).then((assembly) => {
-      this.uppy.log('[Transloadit] Created Assembly')
+      this.uppy.log(`[Transloadit] Created Assembly ${assembly.assembly_id}`)
       return assembly
     }).catch((err) => {
       this.uppy.info(this.i18n('creatingAssemblyFailed'), 'error', 0)
@@ -617,7 +618,7 @@ module.exports = class Transloadit extends Plugin {
 
     let optionsPromise
     if (fileIDs.length > 0) {
-      optionsPromise = this.getAssemblyOptions(fileIDs)
+      optionsPromise = Promise.resolve(this.getAssemblyOptions(fileIDs))
         .then((allOptions) => this.dedupeAssemblyOptions(allOptions))
     } else if (this.opts.alwaysRunAssembly) {
       optionsPromise = Promise.resolve(
@@ -634,9 +635,21 @@ module.exports = class Transloadit extends Plugin {
       return Promise.resolve()
     }
 
-    return optionsPromise.then((assemblies) => Promise.all(
-      assemblies.map(createAssembly)
-    ))
+    return optionsPromise.then(
+      (assemblies) => Promise.all(
+        assemblies.map(createAssembly)
+      ),
+      // If something went wrong before any assemblies could be created,
+      // clear all processing state.
+      (err) => {
+        fileIDs.forEach((fileID) => {
+          const file = this.uppy.getFile(fileID)
+          this.uppy.emit('preprocess-complete', file)
+          this.uppy.emit('upload-error', file, err)
+        })
+        throw err
+      }
+    )
   }
 
   afterUpload (fileIDs, uploadID) {
@@ -771,10 +784,27 @@ module.exports = class Transloadit extends Plugin {
     })
   }
 
+  handleError (err, uploadID) {
+    this.uppy.log('[Transloadit] handleError')
+    this.uppy.log(err)
+    this.uppy.log(uploadID)
+    const state = this.getPluginState()
+    const assemblyIDs = state.uploadsAssemblies[uploadID]
+
+    assemblyIDs.forEach((assemblyID) => {
+      if (this.sockets[assemblyID]) {
+        this.sockets[assemblyID].close()
+      }
+    })
+  }
+
   install () {
     this.uppy.addPreProcessor(this.prepareUpload)
     this.uppy.addPostProcessor(this.afterUpload)
 
+    // We may need to close socket.io connections on error.
+    this.uppy.on('error', this.handleError)
+
     if (this.opts.importFromUploadURLs) {
       // No uploader needed when importing; instead we take the upload URL from an existing uploader.
       this.uppy.on('upload-success', this.onFileUploadURLAvailable)
@@ -783,6 +813,9 @@ module.exports = class Transloadit extends Plugin {
         // Disable tus-js-client fingerprinting, otherwise uploading the same file at different times
         // will upload to the same assembly.
         resume: false,
+        // Disable Uppy Server's retry optimisation; we need to change the endpoint on retry
+        // so it can't just reuse the same tus.Upload instance server-side.
+        useFastRemoteRetry: false,
         // Only send assembly metadata to the tus endpoint.
         metaFields: ['assembly_url', 'filename', 'fieldname']
       })
@@ -806,6 +839,7 @@ module.exports = class Transloadit extends Plugin {
   uninstall () {
     this.uppy.removePreProcessor(this.prepareUpload)
     this.uppy.removePostProcessor(this.afterUpload)
+    this.uppy.off('error', this.handleError)
 
     if (this.opts.importFromUploadURLs) {
       this.uppy.off('upload-success', this.onFileUploadURLAvailable)
@@ -826,3 +860,5 @@ module.exports = class Transloadit extends Plugin {
     })
   }
 }
+
+module.exports.UPPY_SERVER = UPPY_SERVER

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

@@ -164,4 +164,56 @@ describe('Transloadit', () => {
 
     return expect(uppy.upload()).rejects.toEqual(new Error('short-circuited'))
   })
+
+  it('Does not leave lingering progress if getAssemblyOptions fails', () => {
+    const uppy = new Core()
+    uppy.use(Transloadit, {
+      getAssemblyOptions (file) {
+        return Promise.reject(new Error('Failure!'))
+      }
+    })
+
+    uppy.addFile({
+      source: 'jest',
+      name: 'abc',
+      data: new Uint8Array(100)
+    })
+
+    return uppy.upload().then(() => {
+      throw new Error('Should not have succeeded')
+    }, (err) => {
+      const fileID = Object.keys(uppy.getState().files)[0]
+
+      expect(err.message).toBe('Failure!')
+      expect(uppy.getFile(fileID).progress.uploadStarted).toBe(false)
+    })
+  })
+
+  it('Does not leave lingering progress if creating assembly fails', () => {
+    const uppy = new Core()
+    uppy.use(Transloadit, {
+      params: {
+        auth: { key: 'some auth key string' },
+        template_id: 'some template id string'
+      }
+    })
+
+    uppy.getPlugin('Transloadit').client.createAssembly = () =>
+      Promise.reject(new Error('Could not create assembly!'))
+
+    uppy.addFile({
+      source: 'jest',
+      name: 'abc',
+      data: new Uint8Array(100)
+    })
+
+    return uppy.upload().then(() => {
+      throw new Error('Should not have succeeded')
+    }, (err) => {
+      const fileID = Object.keys(uppy.getState().files)[0]
+
+      expect(err.message).toBe('Could not create assembly!')
+      expect(uppy.getFile(fileID).progress.uploadStarted).toBe(false)
+    })
+  })
 })

+ 37 - 16
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')
 
@@ -60,12 +61,21 @@ module.exports = class Tus extends Plugin {
     const defaultOptions = {
       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)
@@ -193,9 +203,6 @@ module.exports = class Tus extends Plugin {
       if (!file.isPaused) {
         upload.start()
       }
-      if (!file.isRestored) {
-        this.uppy.emit('upload-started', file, upload)
-      }
     })
   }
 
@@ -217,8 +224,6 @@ module.exports = class Tus extends Plugin {
           .catch(reject)
       }
 
-      this.uppy.emit('upload-started', file)
-
       fetch(file.remote.url, {
         method: 'post',
         credentials: 'include',
@@ -304,6 +309,17 @@ module.exports = class Tus extends Plugin {
       socket.on('error', (errData) => {
         const { message } = errData.error
         const error = Object.assign(new Error(message), { cause: errData.error })
+
+        // If the remote retry optimisation should not be used,
+        // close the socket—this will tell uppy-server to clear state and delete the file.
+        if (!this.opts.useFastRemoteRetry) {
+          this.resetUploaderReferences(file.id)
+          // Remove the serverToken so that a new one will be created for the retry.
+          this.uppy.setFileState(file.id, {
+            serverToken: null
+          })
+        }
+
         this.uppy.emit('upload-error', file, error)
         reject(error)
       })
@@ -395,23 +411,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)
   }
 

+ 4 - 7
src/scss/_common.scss

@@ -5,13 +5,10 @@
 .uppy-Root {
   all: initial;
   box-sizing: border-box;
-  font-family: -apple-system, BlinkMacSystemFont,
-    'avenir next', avenir,
-    helvetica, 'helvetica neue',
-    ubuntu, roboto, noto,
-    'segoe ui', arial, sans-serif;
+  font-family: system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, 
+    Oxygen, Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
   line-height: 1;
-  // -webkit-font-smoothing: antialiased;
+  -webkit-font-smoothing: antialiased;
 }
 
 .uppy-Root *, .uppy-Root *:before, .uppy-Root *:after {
@@ -201,7 +198,7 @@
   font-family: inherit;
   font-size: 16px;
   line-height: 1;
-  font-weight: 300;
+  font-weight: 500;
   transition: all 0.3s;
   user-select: none;
 }

+ 15 - 9
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;
   }
 
@@ -215,7 +221,7 @@
   font-size: 8px;
   margin-top: 5px;
   margin-bottom: 0;
-  font-weight: normal;
+  font-weight: 500;
   overflow-x: hidden;
   text-overflow: ellipsis;
   white-space: nowrap;
@@ -298,7 +304,7 @@
   // left: 15px;
   font-size: 14px;
   // line-height: 40px;
-  font-weight: 400;
+  font-weight: 500;
   cursor: pointer;
   color: $color-cornflower-blue;
 
@@ -462,7 +468,7 @@
   text-align: center;
   font-size: 18px;
   line-height: 1.45;
-  font-weight: 300;
+  font-weight: 400;
   color: rgba($color-asphalt-gray, 0.8);
   padding: 0 15px;
   // margin: 0;
@@ -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;
   }
 
-// }
+// }

+ 4 - 3
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;
@@ -18,7 +19,7 @@
 .uppy-Provider-authTitle {
   font-size: 20px;
   line-height: 1.4;
-  font-weight: 300;
+  font-weight: 400;
   margin-bottom: 30px;
   padding: 0 15px;
   max-width: 500px;
@@ -81,7 +82,7 @@
 }
 
 .uppy-ProviderBrowser-user {
-  margin: 16px 0;
+  margin: 0 8px 0 0;
 }
 
 .uppy-ProviderBrowser-header {

+ 2 - 2
src/scss/_statusbar.scss

@@ -1,8 +1,8 @@
 .uppy-StatusBar {
   display: flex;
   position: relative;
-  height: 35px;
-  line-height: 35px;
+  height: 40px;
+  line-height: 40px;
   font-size: 12px;
   font-weight: 400;
   color: $color-white;

+ 2 - 1
src/scss/_webcam.scss

@@ -89,12 +89,13 @@
 .uppy-Webcam-Title {
   font-size: 22px;
   line-height: 1.35;
-  font-weight: 300;
+  font-weight: 400;
   margin: 0;
   margin-bottom: 15px;
   padding: 0 15px;
   max-width: 500px;
   text-align: center;
+  color: $color-black;
 }
 
 .uppy-Webcam-permissons p {

+ 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)

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


+ 11 - 11
website/package.json

@@ -3,12 +3,12 @@
   "version": "0.0.1",
   "private": true,
   "hexo": {
-    "version": "3.5.0"
+    "version": "3.7.1"
   },
   "dependencies": {
-    "autoprefixer": "^7.2.5",
+    "autoprefixer": "^7.2.6",
     "cssnano": "^3.10.0",
-    "hexo": "^3.5.0",
+    "hexo": "^3.7.1",
     "hexo-browsersync": "^0.3.0",
     "hexo-deployer-git": "^0.3.1",
     "hexo-generator-alias": "^0.1.3",
@@ -19,20 +19,20 @@
     "hexo-generator-tag": "^0.2.0",
     "hexo-renderer-ejs": "^0.3.1",
     "hexo-renderer-marked": "^0.3.2",
-    "hexo-renderer-postcss": "https://github.com/arturi/hexo-renderer-postcss",
-    "hexo-renderer-scss": "^1.1.0",
+    "hexo-renderer-postcss": "git+https://github.com/arturi/hexo-renderer-postcss.git",
+    "hexo-renderer-scss": "^1.2.0",
     "hexo-server": "^0.3.1",
     "hexo-tag-emojis": "^2.0.1",
     "hexo-util": "^0.6.3",
-    "js-yaml": "^3.10.0",
+    "js-yaml": "^3.11.0",
     "mkdirp": "0.5.1",
-    "postcss-inline-svg": "^3.0.0"
+    "postcss-inline-svg": "^3.1.1"
   },
   "devDependencies": {
     "aliasify": "^2.1.0",
-    "babel-core": "^6.26.0",
+    "babel-core": "^6.26.3",
     "babelify": "^8.0.0",
-    "browserify": "^15.1.0",
+    "browserify": "^16.2.0",
     "chalk": "1.1.3",
     "documentation": "4.0.0-beta9",
     "glob": "7.0.5",
@@ -40,6 +40,6 @@
     "multi-glob": "1.0.1",
     "node-notifier": "^5.2.1",
     "remark": "5.0.1",
-    "watchify": "^3.9.0"
+    "watchify": "^3.11.0"
   }
-}
+}

+ 76 - 15
website/src/docs/aws-s3.md

@@ -151,6 +151,82 @@ uppy-server uses POST uploads by default, but you can also use them with your ow
    })
    ```
 
+## S3 Alternatives
+
+Many other object storage providers have an identical API to S3, so you can use the AwsS3 plugin with them. To use them with Uppy Server, you can set the `UPPYSERVER_AWS_ENDPOINT` variable to the endpoint of your preferred service.
+
+### DigitalOcean Spaces
+
+For example, with DigitalOcean Spaces, you could do something like this:
+
+```bash
+export UPPYSERVER_AWS_ENDPOINT="https://{region}.digitaloceanspaces.com"
+export UPPYSERVER_AWS_BUCKET="my-space-name"
+```
+
+The `{region}` string will be replaced by the contents of the `UPPYSERVER_AWS_REGION` environment variable.
+
+For a working example that you can run and play around with, see the [digitalocean-spaces](https://github.com/transloadit/uppy/tree/master/examples/digitalocean-spaces) folder in the Uppy repository.
+
+### Google Cloud Storage
+
+For Google Cloud Storage, you need to take a few more steps. For the AwsS3 plugin to be able to upload to a GCS bucket, it needs the Interoperability setting enabled. You can enable the Interoperability setting and [generate interoperable storage access keys](https://cloud.google.com/storage/docs/migrating#keys) by going to [Google Cloud Storage](https://console.cloud.google.com/storage) » Settings » Interoperability. Then set the environment variables for Uppy Server like below:
+
+```bash
+export UPPYSERVER_AWS_ENDPOINT="https://storage.googleapis.com"
+export UPPYSERVER_AWS_BUCKET="YOUR-GCS-BUCKET-NAME"
+export UPPYSERVER_AWS_KEY="GOOGxxxxxxxxx" # The Access Key
+export UPPYSERVER_AWS_SECRET="YOUR-GCS-SECRET" # The Secret
+```
+
+You do not need to configure the region with GCS.
+
+You also need to configure CORS differently. Unlike Amazon, Google does not offer a UI for CORS configurations. Instead an HTTP API must be used. If you haven't done this already, see [Configuring CORS on a Bucket](https://cloud.google.com/storage/docs/configuring-cors#Configuring-CORS-on-a-Bucket) in the GCS documentation, or follow the below steps to do it using Google's API playground.
+
+GCS has multiple CORS formats, both XML and JSON. Unfortunately their XML format is different from Amazon's, so we can't simply use the one from the [S3 Bucket configuration](#S3-Bucket-configuration) section. Google appears to favour the JSON format, so we'll use that.
+
+#### JSON CORS Configuration
+
+The JSON format consists of an array of CORS configuration objects. An example using POST policy document uploads is shown here:
+
+```json
+{
+  "cors": [
+    {
+      "origin": ["https://my-app.com"],
+      "method": ["GET", "POST"],
+      "maxAgeSeconds": 3000
+    },
+    {
+      "origin": ["*"],
+      "method": ["GET"],
+      "maxAgeSeconds": 3000
+    }
+  ]
+}
+```
+
+Most AWS configurations should be fairly simple to port to this format. When using presigned `PUT` uploads, replace the `"POST"` method by `"PUT"` in the first entry.
+
+If you have the [gsutil](https://cloud.google.com/storage/docs/gsutil) command-line tool, you can apply this configuration using the [gsutil cors](https://cloud.google.com/storage/docs/configuring-cors#configure-cors-bucket) command.
+
+```bash
+gsutil cors set THAT-FILE.json gs://BUCKET-NAME
+```
+
+Otherwise, you can manually apply it through the OAuth playground:
+
+ 1. Get a temporary API token from the [Google OAuth2.0 playground](https://developers.google.com/oauthplayground/)
+   1. Select the "Cloud Storage JSON API v1" » "devstorage.full_control" scope
+   1. Press "Authorize APIs" and allow access
+ 1. Click "Step 3 - Configure request to API"
+ 1. Configure it like below:
+   - HTTP Method: PATCH
+   - Request URI: `https://www.googleapis.com/storage/v1/b/YOUR_BUCKET_NAME`
+   - Content-Type: application/json (should be the default)
+   - Press "Enter request body" and input your CORS configuration
+ 1. Then, finally, press "Send the request".
+
 ## Examples
 
 <a id="example-presigned-url"></a>
@@ -192,21 +268,6 @@ uppy.use(AwsS3, {
 
 See the [aws-presigned-url example in the uppy repository](https://github.com/transloadit/uppy/tree/master/examples/aws-presigned-url) for a small example that implements both the server-side and the client-side.
 
-### S3 Alternatives
-
-Many other object storage providers have an identical API to S3, so you can use the AwsS3 plugin with them. To use them with uppy-server, you can set the `UPPYSERVER_AWS_ENDPOINT` variable to the endpoint of your preferred service.
-
-For example, with DigitalOcean Spaces, you could do something like this:
-
-```
-export UPPYSERVER_AWS_ENDPOINT="https://{region}.digitaloceanspaces.com"
-export UPPYSERVER_AWS_BUCKET="my-space-name"
-```
-
-The `{region}` string will be replaced by the contents of the `UPPYSERVER_AWS_REGION` environment variable.
-
-For a working example that you can run and play around with, see the [digitalocean-spaces](https://github.com/transloadit/uppy/tree/master/examples/digitalocean-spaces) folder in the Uppy repository.
-
 ### Retrieving presign parameters of the uploaded file
 
 Once the file is uploaded, it's possible to retrieve the parameters that were

+ 15 - 1
website/src/docs/dragdrop.md

@@ -16,7 +16,7 @@ uppy.use(DragDrop, {
   target: null,
   width: '100%',
   height: '100%',
-  note: '',
+  note: null,
   locale: {
     strings: {
       dropHereOr: 'Drop files here or',
@@ -26,6 +26,20 @@ uppy.use(DragDrop, {
 })
 ```
 
+> 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.
+
+### `target: null`
+
+DOM element, CSS selector, or plugin to place the drag and drop area into.
+
+### `width: '100%'`
+
+Drag and drop area width, set in inline CSS, so feel free to use percentage, pixels or other values that you like.
+
+### `height: '100%'`
+
+Drag and drop area height, set in inline CSS, so feel free to use percentage, pixels or other values that you like.
+
 ### `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'`.

+ 5 - 8
website/src/docs/fileinput.md

@@ -13,26 +13,23 @@ permalink: docs/fileinput/
 
 ```js
 uppy.use(FileInput, {
-  target: '.UppyForm',
-  allowMultipleFiles: true,
+  target: null,
   pretty: true,
   inputName: 'files[]',
   locale: {
     strings: {
-      chooseFiles: 'Select to upload'
+      chooseFiles: 'Choose files'
     }
   }
 })
 ```
 
+> 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.
+
 ### `target: null`
 
 DOM element, CSS selector, or plugin to mount the file input into.
 
-### `multipleFiles: true`
-
-Whether to allow the user to select multiple files at once.
-
 ### `pretty: true`
 
 When true, display a styled button (see [example](/examples/xhrupload)) that, when clicked, opens the file selector UI. When false, a plain old browser `<input type="file">` element is shown.
@@ -43,4 +40,4 @@ The `name` attribute for the `<input type="file">` element.
 
 ### `locale: {}`
 
-Custom text to show on the button when `pretty` is true. There is only one string that can be configured: `strings.chooseFiles`.
+Custom text to show on the button when `pretty` is true.

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

@@ -47,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.2/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.2/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:

+ 23 - 0
website/src/docs/transloadit.md

@@ -24,6 +24,29 @@ 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
 
 ### `service`

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

@@ -36,4 +36,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

+ 28 - 24
website/src/docs/uppy.md

@@ -15,10 +15,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 +52,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 +73,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.
@@ -192,9 +196,9 @@ We are using a forked [Polyglot.js](https://github.com/airbnb/polyglot.js/blob/m
 
 ### `store: defaultStore()`
 
-The Store to use to keep track of internal state. By default, a simple object is used.
+The Store to use to keep track of internal state. By [default](/docs/stores/#DefaultStore), a simple object is used.
 
-This option can be used to plug Uppy state into an external state management library, such as Redux. Then, you can write custom views with the library that is also used by the rest of the application.
+This option can be used to plug Uppy state into an external state management library, such as [Redux](/docs/stores/#ReduxStore). Then, you can write custom views with the library that is also used by the rest of the application.
 
 <!-- TODO document store API -->
 
@@ -236,7 +240,7 @@ If `uppy.opts.autoProceed === true`, Uppy will begin uploading automatically whe
 
 ### `uppy.getFile(fileID)`
 
-A shortcut method that returns a specific file object from `uppy.state` by its `fileID`.
+Get a specific file object by its ID.
 
 ```js
 const file = uppy.getFile('uppyteamkongjpg1501851828779')
@@ -286,7 +290,7 @@ uppy.upload().then((result) => {
 
 ### `uppy.setState(patch)`
 
-Update `uppy.state`. Usually this method is called internally, but in some cases it might be useful to alter something in `uppy.state` directly.
+Update Uppy's internal state. Usually this method is called internally, but in some cases it might be useful to alter something directly, especially when implementing your own plugins.
 
 Uppy’s default state on initialization:
 
@@ -316,18 +320,18 @@ uppy.setState({
 })
 ```
 
-We don’t mutate `uppy.state`, so internally `setState` creates a new copy of state and replaces `uppy.state` with it. However, when updating values, it’s your responsibility to not mutate them, but instead create copies. See [Redux docs](http://redux.js.org/docs/recipes/UsingObjectSpreadOperator.html) for more info on this. Here’s an example from Uppy.Core that updates progress for a particular file in state:
+State in Uppy is considered to be immutable. When updating values, it’s your responsibility to not mutate them, but instead create copies. See [Redux docs](http://redux.js.org/docs/recipes/UsingObjectSpreadOperator.html) for more info on this. Here’s an example from Uppy.Core that updates progress for a particular file in state:
 
 ```js
+// We use Object.assign({}, obj) to create a copy of `obj`.
 const updatedFiles = Object.assign({}, uppy.getState().files)
-const updatedFile = Object.assign({}, updatedFiles[fileID],
-  Object.assign({}, {
-    progress: Object.assign({}, updatedFiles[fileID].progress, {
-      bytesUploaded: data.bytesUploaded,
-      bytesTotal: data.bytesTotal,
-      percentage: Math.floor((data.bytesUploaded / data.bytesTotal * 100).toFixed(2))
-    })
-  }
+// We use Object.assign({}, obj, update) to create an altered copy of `obj`.
+const updatedFile = Object.assign({}, updatedFiles[fileID], {
+  progress: Object.assign({}, updatedFiles[fileID].progress, {
+    bytesUploaded: data.bytesUploaded,
+    bytesTotal: data.bytesTotal,
+    percentage: Math.floor((data.bytesUploaded / data.bytesTotal * 100).toFixed(2))
+  })
 ))
 updatedFiles[data.id] = updatedFile
 uppy.setState({files: updatedFiles})
@@ -335,7 +339,7 @@ uppy.setState({files: updatedFiles})
 
 ### `uppy.getState()`
 
-Returns `uppy.state`, which you can also use directly.
+Returns the current state from the [Store](#store-defaultStore).
 
 ### `uppy.setFileState(fileID, state)`
 
@@ -487,7 +491,7 @@ uppy.on('complete', (result) => {
 
 ### `error`
 
-Fired when Uppy fails to upload/encode the whole upload. That error is then set to `uppy.state.error`.
+Fired when Uppy fails to upload/encode the whole upload. That error is then set to `uppy.getState().error`.
 
 ### `upload-error`
 

+ 2 - 0
website/src/docs/webcam.md

@@ -7,6 +7,8 @@ permalink: docs/webcam/
 
 The Webcam plugin lets you take photos and record videos with a built-in camera on desktop and mobile devices.
 
+> 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.
+
 [Try live!](/examples/dashboard/)
 
 ## Options

+ 5 - 2
website/src/docs/xhrupload.md

@@ -183,9 +183,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 +204,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`
 ```
 

+ 4 - 4
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.2/dist/uppy.min.css -->
+<!-- Basic Uppy styles. You can use Transloadit's CDN, Edgly: 
+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.2/dist/uppy.min.js -->
+<!-- Load Uppy pre-built bundled version. You can use Transloadit's CDN, Edgly: 
+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 });

+ 2 - 2
website/themes/uppy/layout/index.ejs

@@ -79,8 +79,8 @@
   <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.2/dist/uppy.min.css" rel="stylesheet">
-<script src="https://transloadit.edgly.net/releases/uppy/v0.24.2/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'

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