Просмотр исходного кода

Merge branch 'master' into feature/split-utils

Artur Paikin 6 лет назад
Родитель
Сommit
d4fbe392c0

+ 30 - 1
CHANGELOG.md

@@ -83,10 +83,10 @@ What we need to do to release Uppy 1.0
 - [ ] QA: add one integration test that uses a Webpack and React/Redux environment (e.g. via `create-react-app`) (@goto-bus-stop)
 - [ ] QA: add one integration test that uses a Provider (investigate if possible with a dedicated Google Drive API key for uppy server, so _with_ oauth dance) (@ife)
 - [ ] QA: add one integration test that uses more exotic (tus) options such as `useFastRemoteRetry` (@arturi)
-- [ ] QA: make it so that all integration tests use `npm pack` and `npm install` first (@ife)
 - [ ] feature: preset for Transloadit that mimics jQuery SDK, check https://github.com/transloadit/jquery-sdk docs (@goto-bus-stop)
 - [ ] feature: basic React Native support (@arturi owner+ios, @ife android)
 - [ ] refactoring: split uppy into small packages, Lerna.js repo? and figure out how to share styles (during work, maybe add PR warning in `.github/*`? use `git mv` for everything) (@goto-bus-stop, @arturi)
+- [x] QA: make it so that all integration tests use `npm pack` and `npm install` first (@ife)
 - [x] docs: on using plugins, all options, list of plugins, i18n
 - [x] feature: beta file recovering after closed tab / browser crash
 - [x] feature: easy integration with React (UppyReact components)
@@ -125,6 +125,35 @@ To Be Released: 2018-06-28.
 - [x] thumbnailgenerator: Polyfill Math.log2 since IE11 doesn't support this method (#892 / @DJWassink)
 - [x] xhrupload: Add `withCredentials` option (#874 / @tuoxiansp)
 
+## 0.25.5
+
+Released: 2018-06-13.
+
+- build: exclude and ignore `node_modules` from `test/endtoend` (@arturi, @kvz / #a60c2f0c641f7db580937ebbc0884e25c8ef8583, #355f696a74d8ec56381578f1fb5ad9c913fe8200)
+
+## 0.25.4
+
+Released: 2018-06-13.
+
+- providers: hanging URL upload (#8e13f416f74e7a453e7bdc829e9618f3b7d68804 / @ifedapoolarewaju)
+- url: fix input focus (#3f9aa3bb7fc7ce5814fe50268a6f88f5965d9f16 / @arturi)
+
+## 0.25.3
+
+Released: 2018-06-12.
+
+- core: fix/refactor `uppy.close()` and `uppy.removePlugin(plugin)`: Remove plugins immutably when uppy.close() is called, not just uninstall; emit event `plugin-remove` before removing plugin; remove plugins from Dashboard when they are removed from Uppy; check if plugin exists in Uppy before re-rendering, since debounced re-render can happen after a plugin is removed, that’s been causing issues in #890 (#898 / @arturi)
+- tests: run integration tests with npm-installed uppy (#880 / @ifedapoolarewaju)
+- xhrupload: add withCredentials option (#874 / @tuoxiansp, @b1ncer)
+- xhrupload: Move .withCredentials assignment to after open(): IE 10 doesn't allow setting it before open() is called (#2698b599d716743bbf7ed3ac70c648fef0fd8976 / @goto-bus-stop)
+- thumbnailgenerator: Updated ThumbnailGenerator to work with IE (#4ddc9da47b13c9dfe49155d8c3bcd76b9fa494f2 / @DJWassink)
+- core: add eslint-plugin-compat (@goto-bus-stop, #894)
+- dashboard: remove Dashboard bottom margin, since “powered by” has been moved (#a561e4e7a2c18f5092ba03185e0836ffa6796d04 / @arturi)
+- dashboard: fix Dashboard open/close animation on small screen (#982d27f62693c0eb026e381d10157afffe1eeb64 / @arturi)
+- awss3: Don't set uploadURL when success_action_status was missing (#900 / @goto-bus-stop)
+- thumbnailgenerator: Add id option to ThumbnailGenerator (#8cded8160b19d3324d9e14be122c4038ed0b9403 / @arturi)
+- react: tiny improvement for Uppy React example (645e15166a6bd100351de131982df080bc71aac6 / @arturi)
+
 ## 0.25.2
 
 Released: 2018-06-05.

+ 5 - 3
README.md

@@ -15,6 +15,8 @@ Uppy is a sleek, modular JavaScript file uploader that integrates seamlessly wit
 
 Uppy is being developed by the folks at [Transloadit](https://transloadit.com), a versatile file encoding service.
 
+⚠️**ATTENTION** ☢️Uppy [is transitioning](https://github.com/transloadit/uppy/issues/862) into a [Lerna repo](https://lernajs.io/) this/next week, please wait with new PRs to avoid conflicts 💛
+
 ## Example
 
 <img width="700" alt="Uppy UI Demo: modal dialog with a few selected files and an upload button" src="https://github.com/transloadit/uppy/raw/master/uppy-screenshot.jpg">
@@ -63,7 +65,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.25.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.25.5/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 +74,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.25.2/dist/uppy.min.js"></script>
+<script src="https://transloadit.edgly.net/releases/uppy/v0.25.5/dist/uppy.min.js"></script>
 ```
 
 2\. Add CSS to `<head>`:
 ``` html
-<link href="https://transloadit.edgly.net/releases/uppy/v0.25.2/dist/uppy.min.css" rel="stylesheet">
+<link href="https://transloadit.edgly.net/releases/uppy/v0.25.5/dist/uppy.min.css" rel="stylesheet">
 ```
 
 3\. Initialize:

+ 2 - 1
bin/endtoend-build

@@ -19,4 +19,5 @@ rm ./test/endtoend/package-lock.json
 
 cp ./test/endtoend/node_modules/uppy/dist/uppy.min.css ./test/endtoend/dist
 cp ./test/endtoend/src/index.html ./test/endtoend/dist
-browserify ./test/endtoend/src/main.js -o ./test/endtoend/dist/bundle.js -t babelify
+browserify ./test/endtoend/src/main.js -o ./test/endtoend/dist/bundle.js -t babelify
+rm -rf ./test/endtoend/node_modules

+ 2 - 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.25.2/dist/uppy.css
+#  - Runs npm pack, and stores files to e.g. https://transloadit.edgly.net/releases/uppy/v0.25.5/dist/uppy.css
 #  - Uses local package by default, if [version] argument was specified, takes package from npm
 #
 # Run as:
@@ -98,6 +98,7 @@ pushd "${__root}" > /dev/null 2>&1
       --region="us-east-1" \
       --exclude 'website/*' \
       --exclude 'node_modules/*' \
+      --exclude 'test/*/node_modules/*' \
       --exclude 'examples/*/node_modules/*' \
     ./ "s3://crates.edgly.net/756b8efaed084669b02cb99d4540d81f/default/releases/uppy/v${version}"
     echo "Saved https://transloadit.edgly.net/releases/uppy/v${version}/"

+ 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.25.2/dist/uppy.min.css" rel="stylesheet">
+    <link href="https://transloadit.edgly.net/releases/uppy/v0.25.5/dist/uppy.min.css" rel="stylesheet">
   </head>
   <body>
     <button id="uppyModalOpener">Open Modal</button>
-    <script src="https://transloadit.edgly.net/releases/uppy/v0.25.2/dist/uppy.min.js"></script>
+    <script src="https://transloadit.edgly.net/releases/uppy/v0.25.5/dist/uppy.min.js"></script>
     <script>
       const uppy = Uppy.Core({debug: true, autoProceed: false})
         .use(Uppy.Dashboard, { trigger: '#uppyModalOpener' })

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

@@ -18,11 +18,11 @@ module.exports = class App extends React.Component {
   }
 
   componentWillMount () {
-    this.uppy = new Uppy({ autoProceed: false })
+    this.uppy = new Uppy({ id: 'uppy1', autoProceed: true, debug: true })
       .use(Tus, { endpoint: 'https://master.tus.io/files/' })
       .use(GoogleDrive, { host: 'https://server.uppy.io' })
 
-    this.uppy2 = new Uppy({ autoProceed: false })
+    this.uppy2 = new Uppy({ id: 'uppy2', autoProceed: false, debug: true })
       .use(Tus, { endpoint: 'https://master.tus.io/files/' })
   }
 
@@ -93,6 +93,7 @@ module.exports = class App extends React.Component {
         <h2>Progress Bar</h2>
         <ProgressBar
           uppy={this.uppy}
+          hideAfterFinish={false}
         />
       </div>
     )

+ 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.25.2/dist/uppy.min.css" rel="stylesheet">
+    <link href="https://transloadit.edgly.net/releases/uppy/v0.25.5/dist/uppy.min.css" rel="stylesheet">
   </head>
   <body>
     <button id="uppyModalOpener">Open Modal</button>
-    <script src="https://transloadit.edgly.net/releases/uppy/v0.25.2/dist/uppy.min.js"></script>
+    <script src="https://transloadit.edgly.net/releases/uppy/v0.25.5/dist/uppy.min.js"></script>
     <script>
       const uppy = Uppy.Core({debug: true, autoProceed: false})
         .use(Uppy.Dashboard, { trigger: '#uppyModalOpener' })

+ 59 - 1
package-lock.json

@@ -1,6 +1,6 @@
 {
   "name": "uppy",
-  "version": "0.25.2",
+  "version": "0.25.5",
   "lockfileVersion": 1,
   "requires": true,
   "dependencies": {
@@ -12933,6 +12933,58 @@
       "integrity": "sha1-ICtIAhoMTL3i34DeFaF0Q8i0OYA=",
       "dev": true
     },
+    "nock": {
+      "version": "9.3.2",
+      "resolved": "https://registry.npmjs.org/nock/-/nock-9.3.2.tgz",
+      "integrity": "sha512-pulpsRVFneYGpgktmt99s10fFh10zSpYhydwkG28xLps/p19n39lBSq5kjb7UW2YOPyQtt7FLyXuP+xHyRRI0w==",
+      "dev": true,
+      "requires": {
+        "chai": "^4.1.2",
+        "debug": "^3.1.0",
+        "deep-equal": "^1.0.0",
+        "json-stringify-safe": "^5.0.1",
+        "lodash": "^4.17.5",
+        "mkdirp": "^0.5.0",
+        "propagate": "^1.0.0",
+        "qs": "^6.5.1",
+        "semver": "^5.5.0"
+      },
+      "dependencies": {
+        "debug": {
+          "version": "3.1.0",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
+          "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
+          "dev": true,
+          "requires": {
+            "ms": "2.0.0"
+          }
+        },
+        "lodash": {
+          "version": "4.17.10",
+          "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.10.tgz",
+          "integrity": "sha512-UejweD1pDoXu+AD825lWwp4ZGtSwgnpZxb3JDViD7StjQz+Nb/6l093lx4OQ0foGWNRoc19mWy7BzL+UAK2iVg==",
+          "dev": true
+        },
+        "ms": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+          "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=",
+          "dev": true
+        },
+        "qs": {
+          "version": "6.5.2",
+          "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz",
+          "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==",
+          "dev": true
+        },
+        "semver": {
+          "version": "5.5.0",
+          "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz",
+          "integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==",
+          "dev": true
+        }
+      }
+    },
     "node-fetch": {
       "version": "1.7.1",
       "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-1.7.1.tgz",
@@ -14736,6 +14788,12 @@
         "object-assign": "^4.1.1"
       }
     },
+    "propagate": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/propagate/-/propagate-1.0.0.tgz",
+      "integrity": "sha1-AMLa7t2iDofjeCs0Stuhzd1q1wk=",
+      "dev": true
+    },
     "property-is-enumerable-x": {
       "version": "1.1.0",
       "resolved": "https://registry.npmjs.org/property-is-enumerable-x/-/property-is-enumerable-x-1.1.0.tgz",

+ 2 - 1
package.json

@@ -1,6 +1,6 @@
 {
   "name": "uppy",
-  "version": "0.25.2",
+  "version": "0.25.5",
   "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",
@@ -87,6 +87,7 @@
     "mkdirp": "0.5.1",
     "multi-glob": "1.0.1",
     "next-update": "^3.6.0",
+    "nock": "^9.3.2",
     "node-sass": "^4.9.0",
     "npm-run-all": "^4.1.2",
     "onchange": "^3.3.0",

+ 14 - 4
src/core/Core.js

@@ -890,28 +890,37 @@ class Uppy {
    * @param {object} instance The plugin instance to remove.
    */
   removePlugin (instance) {
-    const list = this.plugins[instance.type]
+    this.log(`Removing plugin ${instance.id}`)
+    this.emit('plugin-remove', instance)
 
     if (instance.uninstall) {
       instance.uninstall()
     }
 
+    const list = this.plugins[instance.type].slice()
     const index = list.indexOf(instance)
     if (index !== -1) {
       list.splice(index, 1)
+      this.plugins[instance.type] = list
     }
+
+    const updatedState = this.getState()
+    delete updatedState.plugins[instance.id]
+    this.setState(updatedState)
   }
 
   /**
    * Uninstall all plugins and close down this Uppy instance.
    */
   close () {
+    this.log(`Closing Uppy instance ${this.opts.id}: removing all files and uninstalling plugins`)
+
     this.reset()
 
     this._storeUnsubscribe()
 
     this.iteratePlugins((plugin) => {
-      plugin.uninstall()
+      this.removePlugin(plugin)
     })
   }
 
@@ -1197,8 +1206,9 @@ class Uppy {
       })
       .catch((err) => {
         const message = typeof err === 'object' ? err.message : err
-        this.log(message)
-        this.info(message, 'error', 4000)
+        const details = typeof err === 'object' ? err.details : null
+        this.log(`${message} ${details}`)
+        this.info({ message: message, details: details }, 'error', 4000)
         return Promise.reject(typeof err === 'object' ? err : new Error(err))
       })
   }

+ 4 - 5
src/core/Core.test.js

@@ -255,10 +255,10 @@ describe('src/Core', () => {
     const core = new Core()
     core.use(AcquirerPlugin1)
 
-    // const corePauseEventMock = jest.fn()
     const coreCancelEventMock = jest.fn()
     const coreStateUpdateEventMock = jest.fn()
-    // core.on('pause-all', corePauseEventMock)
+    const plugin = core.plugins.acquirer[0]
+
     core.on('cancel-all', coreCancelEventMock)
     core.on('state-update', coreStateUpdateEventMock)
 
@@ -277,9 +277,8 @@ describe('src/Core', () => {
       plugins: {},
       totalProgress: 0
     })
-    expect(core.plugins.acquirer[0].mocks.uninstall.mock.calls.length).toEqual(
-      1
-    )
+    expect(plugin.mocks.uninstall.mock.calls.length).toEqual(1)
+    expect(core.plugins[Object.keys(core.plugins)[0]].length).toEqual(0)
   })
 
   describe('upload hooks', () => {

+ 5 - 1
src/core/Plugin.js

@@ -85,6 +85,10 @@ module.exports = class Plugin {
 
       // API for plugins that require a synchronous rerender.
       this.rerender = (state) => {
+        // plugin could be removed, but this.rerender is debounced below,
+        // so it could still be called even after uppy.removePlugin or uppy.close
+        // hence the check
+        if (!this.uppy.getPlugin(this.id)) return
         this.el = preact.render(this.render(state), targetElement, this.el)
       }
       this._updateUI = debounce(this.rerender)
@@ -137,7 +141,7 @@ module.exports = class Plugin {
   }
 
   unmount () {
-    if (this.el && this.el.parentNode) {
+    if (this.isTargetDOMEl && this.el && this.el.parentNode) {
       this.el.parentNode.removeChild(this.el)
     }
   }

+ 0 - 41
src/core/PromiseWaiter.js

@@ -1,41 +0,0 @@
-/**
- * Wait for multiple Promises to resolve.
- */
-module.exports = class PromiseWaiter {
-  constructor () {
-    this.promises = []
-  }
-
-  add (promise) {
-    this.promises.push(promise)
-
-    const remove = () => {
-      this.remove(promise)
-    }
-    promise.then(remove, remove)
-  }
-
-  remove (promise) {
-    const index = this.promises.indexOf(promise)
-    if (index !== -1) {
-      this.promises.splice(index, 1)
-    }
-  }
-
-  wait () {
-    const promises = this.promises
-    this.promises = []
-
-    function noop () {
-      // No result value
-    }
-
-    // Just wait for a Promise to conclude in some way, whether it's resolution
-    // or rejection. We don't care about the contents.
-    function concluded (promise) {
-      return promise.then(noop, noop)
-    }
-
-    return Promise.all(promises.map(concluded)).then(noop)
-  }
-}

+ 6 - 6
src/plugins/AwsS3/Multipart.js

@@ -40,7 +40,7 @@ module.exports = class AwsS3Multipart extends Plugin {
     this.type = 'uploader'
     this.id = 'AwsS3Multipart'
     this.title = 'AWS S3 Multipart'
-    this.server = new RequestClient(uppy, opts)
+    this.client = new RequestClient(uppy, opts)
 
     const defaultOptions = {
       timeout: 30 * 1000,
@@ -95,7 +95,7 @@ module.exports = class AwsS3Multipart extends Plugin {
   createMultipartUpload (file) {
     this.assertHost()
 
-    return this.server.post('s3/multipart', {
+    return this.client.post('s3/multipart', {
       filename: file.name,
       type: file.type
     }).then(assertServerError)
@@ -105,7 +105,7 @@ module.exports = class AwsS3Multipart extends Plugin {
     this.assertHost()
 
     const filename = encodeURIComponent(key)
-    return this.server.get(`s3/multipart/${uploadId}?key=${filename}`)
+    return this.client.get(`s3/multipart/${uploadId}?key=${filename}`)
       .then(assertServerError)
   }
 
@@ -113,7 +113,7 @@ module.exports = class AwsS3Multipart extends Plugin {
     this.assertHost()
 
     const filename = encodeURIComponent(key)
-    return this.server.get(`s3/multipart/${uploadId}/${number}?key=${filename}`)
+    return this.client.get(`s3/multipart/${uploadId}/${number}?key=${filename}`)
       .then(assertServerError)
   }
 
@@ -122,7 +122,7 @@ module.exports = class AwsS3Multipart extends Plugin {
 
     const filename = encodeURIComponent(key)
     const uploadIdEnc = encodeURIComponent(uploadId)
-    return this.server.post(`s3/multipart/${uploadIdEnc}/complete?key=${filename}`, { parts })
+    return this.client.post(`s3/multipart/${uploadIdEnc}/complete?key=${filename}`, { parts })
       .then(assertServerError)
   }
 
@@ -131,7 +131,7 @@ module.exports = class AwsS3Multipart extends Plugin {
 
     const filename = encodeURIComponent(key)
     const uploadIdEnc = encodeURIComponent(uploadId)
-    return this.server.delete(`s3/multipart/${uploadIdEnc}?key=${filename}`)
+    return this.client.delete(`s3/multipart/${uploadIdEnc}?key=${filename}`)
       .then(assertServerError)
   }
 

+ 20 - 0
src/plugins/AwsS3/index.js

@@ -148,17 +148,33 @@ module.exports = class AwsS3 extends Plugin {
   }
 
   install () {
+    const { log } = this.uppy
     this.uppy.addPreProcessor(this.prepareUpload)
 
+    let warnedSuccessActionStatus = false
     this.uppy.use(XHRUpload, {
       fieldName: 'file',
       responseUrlFieldName: 'location',
       timeout: this.opts.timeout,
       limit: this.opts.limit,
+      // Get the response data from a successful XMLHttpRequest instance.
+      // `content` is the S3 response as a string.
+      // `xhr` is the XMLHttpRequest instance.
       getResponseData (content, xhr) {
+        const opts = this
+
         // If no response, we've hopefully done a PUT request to the file
         // in the bucket on its full URL.
         if (!isXml(xhr)) {
+          if (opts.method.toUpperCase() === 'POST') {
+            if (!warnedSuccessActionStatus) {
+              log('[AwsS3] No response data found, make sure to set the success_action_status AWS SDK option to 201. See https://uppy.io/docs/aws-s3/#POST-Uploads', 'warning')
+              warnedSuccessActionStatus = true
+            }
+            // The responseURL won't contain the object key. Give up.
+            return { location: null }
+          }
+
           // Trim the query string because it's going to be a bunch of presign
           // parameters for a PUT request—doing a GET request with those will
           // always result in an error
@@ -192,6 +208,10 @@ module.exports = class AwsS3 extends Plugin {
           etag: getValue('ETag')
         }
       },
+
+      // Get the error data from a failed XMLHttpRequest instance.
+      // `content` is the S3 response as a string.
+      // `xhr` is the XMLHttpRequest instance.
       getResponseError (content, xhr) {
         // If no response, we don't have a specific error message, use the default.
         if (!isXml(xhr)) {

+ 15 - 0
src/plugins/Dashboard/index.js

@@ -132,6 +132,7 @@ module.exports = class Dashboard extends Plugin {
     this.isModalOpen = this.isModalOpen.bind(this)
 
     this.addTarget = this.addTarget.bind(this)
+    this.removeTarget = this.removeTarget.bind(this)
     this.hideAllPanels = this.hideAllPanels.bind(this)
     this.showPanel = this.showPanel.bind(this)
     this.getFocusableNodes = this.getFocusableNodes.bind(this)
@@ -150,6 +151,16 @@ module.exports = class Dashboard extends Plugin {
     this.install = this.install.bind(this)
   }
 
+  removeTarget (plugin) {
+    const pluginState = this.getPluginState()
+    // filter out the one we want to remove
+    const newTargets = pluginState.targets.filter(target => target.id !== plugin.id)
+
+    this.setPluginState({
+      targets: newTargets
+    })
+  }
+
   addTarget (plugin) {
     const callerPluginId = plugin.id || plugin.constructor.name
     const callerPluginName = plugin.title || callerPluginId
@@ -363,6 +374,8 @@ module.exports = class Dashboard extends Plugin {
 
     this.updateDashboardElWidth()
     window.addEventListener('resize', this.updateDashboardElWidth)
+
+    this.uppy.on('plugin-remove', this.removeTarget)
   }
 
   removeEvents () {
@@ -373,6 +386,8 @@ module.exports = class Dashboard extends Plugin {
 
     this.removeDragDropListener()
     window.removeEventListener('resize', this.updateDashboardElWidth)
+
+    this.uppy.off('plugin-remove', this.removeTarget)
   }
 
   updateDashboardElWidth () {

+ 1 - 1
src/plugins/Instagram/index.js

@@ -90,7 +90,7 @@ module.exports = class Instagram extends Plugin {
         <path d="M36.537 28.156l-11-7a1.005 1.005 0 0 0-1.02-.033C24.2 21.3 24 21.635 24 22v14a1 1 0 0 0 1.537.844l11-7a1.002 1.002 0 0 0 0-1.688zM26 34.18V23.82L34.137 29 26 34.18z" /><path d="M57 6H1a1 1 0 0 0-1 1v44a1 1 0 0 0 1 1h56a1 1 0 0 0 1-1V7a1 1 0 0 0-1-1zM10 28H2v-9h8v9zm-8 2h8v9H2v-9zm10 10V8h34v42H12V40zm44-12h-8v-9h8v9zm-8 2h8v9h-8v-9zm8-22v9h-8V8h8zM2 8h8v9H2V8zm0 42v-9h8v9H2zm54 0h-8v-9h8v9z" />
       </svg>
     }
-    return <img src={item.images.thumbnail.url} />
+    return <img src={item.images.low_resolution.url} />
   }
 
   getItemSubList (item) {

+ 1 - 1
src/plugins/ThumbnailGenerator/index.js

@@ -11,7 +11,7 @@ module.exports = class ThumbnailGenerator extends Plugin {
   constructor (uppy, opts) {
     super(uppy, opts)
     this.type = 'thumbnail'
-    this.id = 'ThumbnailGenerator'
+    this.id = this.opts.id || 'ThumbnailGenerator'
     this.title = 'Thumbnail Generator'
     this.queue = []
     this.queueProcessing = false

+ 3 - 3
src/plugins/Transloadit/Client.js

@@ -36,9 +36,9 @@ module.exports = class Client {
       body: data
     }).then((response) => response.json()).then((assembly) => {
       if (assembly.error) {
-        const error = new Error(assembly.message)
-        error.code = assembly.error
-        error.status = assembly
+        const error = new Error(assembly.error)
+        error.message = assembly.error
+        error.details = assembly.reason
         throw error
       }
 

+ 6 - 1
src/plugins/Transloadit/index.js

@@ -231,7 +231,8 @@ module.exports = class Transloadit extends Plugin {
       this.uppy.log(`[Transloadit] Created Assembly ${assembly.assembly_id}`)
       return assembly
     }).catch((err) => {
-      this.uppy.info(this.i18n('creatingAssemblyFailed'), 'error', 0)
+      // this.uppy.info(this.i18n('creatingAssemblyFailed'), 'error', 0)
+      err.message = `${this.i18n('creatingAssemblyFailed')}: ${err.message}`
 
       // Reject the promise.
       throw err
@@ -725,6 +726,10 @@ module.exports = class Transloadit extends Plugin {
         }
         this.uppy.log(`[Transloadit] afterUpload(): Got Assembly error ${assembly.assembly_id}`)
         this.uppy.log(error)
+        // this.uppy.info({
+        //   message: error.code,
+        //   details: error.status.reason
+        // }, 'error', 5000)
 
         // Clear postprocessing state for all our files.
         const files = this.getAssemblyFiles(assembly.assembly_id)

+ 2 - 2
src/plugins/Transloadit/index.test.js

@@ -199,7 +199,7 @@ describe('Transloadit', () => {
     })
 
     uppy.getPlugin('Transloadit').client.createAssembly = () =>
-      Promise.reject(new Error('Could not create assembly!'))
+      Promise.reject(new Error('VIDEO_ENCODE_VALIDATION'))
 
     uppy.addFile({
       source: 'jest',
@@ -212,7 +212,7 @@ describe('Transloadit', () => {
     }, (err) => {
       const fileID = Object.keys(uppy.getState().files)[0]
 
-      expect(err.message).toBe('Could not create assembly!')
+      expect(err.message).toBe('Transloadit: Could not create Assembly: VIDEO_ENCODE_VALIDATION')
       expect(uppy.getFile(fileID).progress.uploadStarted).toBe(false)
     })
   })

+ 4 - 3
src/plugins/Tus.js

@@ -1,7 +1,7 @@
 const Plugin = require('../core/Plugin')
 const tus = require('tus-js-client')
 const UppySocket = require('../core/UppySocket')
-const Provider = require('../server/Provider')
+const { Provider, RequestClient } = require('../server')
 const emitSocketProgress = require('../utils/emitSocketProgress')
 const getSocketHost = require('../utils/getSocketHost')
 const settle = require('../utils/settle')
@@ -238,8 +238,9 @@ module.exports = class Tus extends Plugin {
       }
 
       this.uppy.emit('upload-started', file)
-      const provider = new Provider(this.uppy, file.remote.providerOptions)
-      provider.post(
+      const Client = file.remote.providerOptions.provider ? Provider : RequestClient
+      const client = new Client(this.uppy, file.remote.providerOptions)
+      client.post(
         file.remote.url,
         Object.assign({}, file.remote.body, {
           endpoint: opts.endpoint,

+ 1 - 1
src/plugins/Url/UrlUI.js

@@ -13,7 +13,7 @@ class UrlUI extends Component {
     // Component is mounted right away, but the tab panel might be animating
     // still, so input element is positioned outside viewport. This fixes it.
     setTimeout(() => {
-      if (!this.connectButton) return
+      if (!this.input) return
       this.input.focus({ preventScroll: true })
     }, 150)
   }

+ 4 - 3
src/plugins/Url/index.js

@@ -60,7 +60,7 @@ module.exports = class Url extends Plugin {
 
     this.handlePaste = this.handlePaste.bind(this)
 
-    this.server = new RequestClient(uppy, {host: this.opts.host})
+    this.client = new RequestClient(uppy, {host: this.opts.host})
   }
 
   getFileNameFromUrl (url) {
@@ -89,7 +89,7 @@ module.exports = class Url extends Plugin {
   }
 
   getMeta (url) {
-    return this.server.post('url/meta', { url })
+    return this.client.post('url/meta', { url })
       .then((res) => {
         if (res.error) {
           this.uppy.log('[URL] Error:')
@@ -127,7 +127,8 @@ module.exports = class Url extends Plugin {
             body: {
               fileId: url,
               url: url
-            }
+            },
+            providerOptions: this.client.opts
           }
         }
         return tagFile

+ 37 - 0
src/plugins/XHRUpload.test.js

@@ -0,0 +1,37 @@
+const nock = require('nock')
+const Core = require('../core')
+const XHRUpload = require('./XHRUpload')
+
+describe('XHRUpload', () => {
+  describe('getResponseData', () => {
+    it('has the XHRUpload options as its `this`', () => {
+      nock('https://fake-endpoint.uppy.io')
+        .defaultReplyHeaders({
+          'access-control-allow-method': 'POST',
+          'access-control-allow-origin': '*'
+        })
+        .options('/').reply(200, {})
+        .post('/').reply(200, {})
+
+      const core = new Core({ autoProceed: false })
+      const getResponseData = jest.fn(function () {
+        expect(this.some).toEqual('option')
+        return {}
+      })
+      core.use(XHRUpload, {
+        id: 'XHRUpload',
+        endpoint: 'https://fake-endpoint.uppy.io',
+        some: 'option',
+        getResponseData
+      })
+      core.addFile({
+        name: 'test.jpg',
+        data: new Blob([Buffer.alloc(8192)])
+      })
+
+      return core.upload().then(() => {
+        expect(getResponseData).toHaveBeenCalled()
+      })
+    })
+  })
+})

+ 0 - 1
src/scss/_dashboard.scss

@@ -74,7 +74,6 @@
   left: 0;
   right: 0;
   bottom: 0;
-  // background-color: rgba($color-white, 0.8);
   background-color: rgba($color-black, 0.5);
   z-index: $zIndex-2;
 }

+ 8 - 4
src/scss/_informer.scss

@@ -56,7 +56,11 @@
   margin-left: -1px;
 }
 
-.uppy-Informer span:after {
-  line-height: 1.3;
-  word-wrap: break-word;
-}
+  .uppy-Informer span:hover {
+    cursor: help;
+  }
+
+  .uppy-Informer span:after {
+    line-height: 1.3;
+    word-wrap: break-word;
+  }

+ 17 - 8
src/server/RequestClient.js

@@ -43,6 +43,13 @@ module.exports = class RequestClient {
     return response
   }
 
+  _getUrl (url) {
+    if (/^(https?:|)\/\//.test(url)) {
+      return url
+    }
+    return `${this.hostname}/${url}`
+  }
+
   get (path) {
     return fetch(this._getUrl(path), {
       method: 'get',
@@ -51,6 +58,9 @@ module.exports = class RequestClient {
       // @todo validate response status before calling json
       .then(this.onReceiveResponse)
       .then((res) => res.json())
+      .catch((err) => {
+        throw new Error(`Could not get ${this._getUrl(path)}. ${err}`)
+      })
   }
 
   post (path, data) {
@@ -62,17 +72,13 @@ module.exports = class RequestClient {
       .then(this.onReceiveResponse)
       .then((res) => {
         if (res.status < 200 || res.status > 300) {
-          throw new Error(res.statusText)
+          throw new Error(`Could not post ${this._getUrl(path)}. ${res.statusText}`)
         }
         return res.json()
       })
-  }
-
-  _getUrl (url) {
-    if (/^(https?:|)\/\//.test(url)) {
-      return url
-    }
-    return `${this.hostname}/${url}`
+      .catch((err) => {
+        throw new Error(`Could not post ${this._getUrl(path)}. ${err}`)
+      })
   }
 
   delete (path, data) {
@@ -88,5 +94,8 @@ module.exports = class RequestClient {
       .then(this.onReceiveResponse)
       // @todo validate response status before calling json
       .then((res) => res.json())
+      .catch((err) => {
+        throw new Error(`Could not delete ${this._getUrl(path)}. ${err}`)
+      })
   }
 }

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

@@ -457,7 +457,7 @@ module.exports = class ProviderView {
     const handleToken = (e) => {
       const allowedOrigin = new RegExp(noProtocol(this.plugin.opts.hostPattern))
       if (!allowedOrigin.test(noProtocol(e.origin)) || e.source !== authWindow) {
-        console.log(`rejecting event from ${e.origin} vs allowed pattern ${this.plugin.opts.hostPattern}`)
+        this.plugin.uppy.log(`rejecting event from ${e.origin} vs allowed pattern ${this.plugin.opts.hostPattern}`)
         return
       }
       authWindow.close()

+ 1 - 0
test/endtoend/.npmignore

@@ -1 +1,2 @@
 dist
+node_modules

+ 75 - 0
website/src/_posts/2018-06-0.25.md

@@ -0,0 +1,75 @@
+---
+title: "Uppy 0.25: Drag drop links or images from webpages, improved selecting in providers, interactive components in i18n strings"
+date: 2018-06-07
+author: renee
+image: ""
+published: false
+---
+
+Uppy 0.25 brings drag drop links or images from webpages, improved selecting in provider view, interactive components in i18n strings.
+
+<!--more-->
+
+## i18n Strings With Interactive Components
+
+⚠️ **breaking**
+
+We’ve changed how i18n strings work with interactive components. We now use placeholder for those interactive buttons, so it works nicely in languages that have different word order than English. Example:
+
+```js
+dropPasteImport: 'Drop files here, paste, import from one of the locations above or %{browse}'
+browse: 'browse'
+```
+
+`%{browse}` will be replaced with a button/link.
+
+We can’t do this in all situations yet, because some parts of some strings need to be wrapped in Preact elements, which the Translator can’t handle.
+
+Here’s a list of strings that were changed in this release, please update those in your locales:
+
+- core: `failedToUpload` needs to contain `%{file}`, substituted by the name of the file that failed
+- dashboard: `dropPaste` and `dropPasteImport` need to contain `%{browse}`, substituted by the "browse" text button
+- dashboard: `editing` needs to contain `%{file}`, substituted by the name of the file being edited
+- dashboard: `fileSource` and `importFrom` need to contain `%{name}`, substituted by the name of the provider
+- dragdrop: `dropHereOr` needs to contain `%{browse}`, substituted by the "browse" text button
+
+See full list of locale strings for each plugin you are using on [Uppy Docs](/docs/).
+
+## React Documentation
+
+React docs have been improved: we now have a separate page for each Uppy React wrapper component we offer.
+
+- The id option is listed on each plugin's documentation page.
+- The replaceTargetContent option is listed on UI plugin documentation pages.
+- The locale option is described on each plugin documentation page, and includes a short description for each string. The default value is removed from the snippet at the top of the ## Options section to save some space (since it's a very large object sometimes).
+
+## Drag & Drop Links or Images From Webpages
+
+<figure class="wide"><video alt="Demo video showing Uppy with Url plugin that accepts drag and dropped urls" controls autoplay><source src="/images/blog/0.25/link-drop-demo.mp4" type="video/mp4">Your browser does not support the video tag, you can <a href="/images/blog/0.25/link-drop-demo.mp4">download the video</a> to watch it.</video></figure>
+
+## Select In Provider View Vefore Adding Files
+
+Selecting files doesn’t add files to Uppy immediately anymore. Instead a “Select” button appears (with a counter), and pressing that adds all the files with checked checkboxes and closes the Instagram or Google Drive overlay. Cancel discards the selection and also closes the overlay.
+
+This addresses an undesirable case where `autoProceed: true` would begin uploading files the moment you ticked a checkbox, and you couldn’t see what was going on because the provider overlay was still open.
+
+## Typescript Definitions
+
+...
+
+## Other cool things
+
+- Fix: Debounce render calls again, fixes #669 #796 by goto-bus-stop
+- Fix: XHRUPload canceled uploads progress events #864 
+- Improvement: ⚠️ **breaking** Remove `uppy.run()` — everything is now run automatically, you only need to call `.use()` for plugins that you need, as usual (#793 / @goto-bus-stop)
+- Improvement: Dashboard open/close animation
+- Improvement: You can now hide action buttons in Dashboard and StatusBar #821
+- Improvement: Pass `allowedFileTypes` and `maxNumberOfFiles` to `input[type=file]` to add restrictions to the system file picking dialog too
+- Improvement: merge meta data when add file #810
+- Fix: More robust failure handling for Transloadit, closes #708 (#805)
+
+<img class="border" src="/images/blog/0.25/">
+
+Have fun,
+
+The Uppy Team

+ 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.25.2/dist/uppy.min.js"></script>
+<script src="https://transloadit.edgly.net/releases/uppy/v0.25.5/dist/uppy.min.js"></script>
 ```
 
 2\. Add CSS to `<head>`:
 ``` html
-<link href="https://transloadit.edgly.net/releases/uppy/v0.25.2/dist/uppy.min.css" rel="stylesheet">
+<link href="https://transloadit.edgly.net/releases/uppy/v0.25.5/dist/uppy.min.css" rel="stylesheet">
 ```
 
 3\. Initialize:

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

@@ -12,7 +12,7 @@ Plugins are what makes Uppy useful: they help select, manipulate and upload file
   - [DragDrop](/docs/dragdrop) — plain and simple drag-and-drop area
   - [FileInput](/docs/fileinput) — even more plain and simple, just a button
   - [Webcam](/docs/webcam) — upload selfies or audio / video recordings
-  - [Provider Plugins](/docs/providers) (remote sources that work through [Uppy Server](/docs/uppy-server/))
+  - [Provider Plugins](/docs/providers) (remote sources that work through [Uppy Server](/docs/server/))
     - [Dropbox](/docs/dropbox) – import files from Dropbox
     - [GoogleDrive](/docs/google-drive) – import files from Google Drive
     - [Instagram](/docs/instagram) – import files from Instagram

+ 2 - 2
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.25.2/dist/uppy.min.css -->
+https://transloadit.edgly.net/releases/uppy/v0.25.5/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.25.2/dist/uppy.min.js -->
+https://transloadit.edgly.net/releases/uppy/v0.25.5/dist/uppy.min.js -->
 <script src="/uppy/uppy.min.js"></script>
 <script>
   var uppy = Uppy.Core({ debug: true });

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

@@ -55,7 +55,7 @@ function initUppy () {
       inline: true,
       maxHeight: 400,
       target: '#uppy-dashboard-container',
-      note: 'Images and video only, 1–2 files, up to 1 MB'
+      note: 'Images only, 1–2 files, up to 1 MB'
     })
     .use(Instagram, { target: Dashboard, host: 'https://api2.transloadit.com/uppy-server' })
     .use(Webcam, { target: Dashboard })

BIN
website/src/images/blog/0.25/link-drop-demo.mp4


+ 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.25.2/dist/uppy.min.css" rel="stylesheet">
-<script src="https://transloadit.edgly.net/releases/uppy/v0.25.2/dist/uppy.min.js"></script>
+<link href="https://transloadit.edgly.net/releases/uppy/v0.25.5/dist/uppy.min.css" rel="stylesheet">
+<script src="https://transloadit.edgly.net/releases/uppy/v0.25.5/dist/uppy.min.js"></script>
 
 <script>
   var PROTOCOL = location.protocol === 'https:' ? 'https' : 'http'