Procházet zdrojové kódy

Merge branch 'master' into status-bar/add-spinner

Artur Paikin před 6 roky
rodič
revize
9ee57bb9bd
58 změnil soubory, kde provedl 764 přidání a 201 odebrání
  1. 10 20
      .travis.yml
  2. 5 0
      CHANGELOG.md
  3. 4 4
      README.md
  4. binární
      assets/developed-by-transloadit.png
  5. binární
      assets/uppy-demo-oct-2018.gif
  6. binární
      assets/uppy-demo-oct-2018.mov
  7. binární
      assets/uppy-demo-oct-2018.mov-palette.png
  8. binární
      assets/uppy-demo-oct-2018.mp4
  9. binární
      assets/uppy-demo2.mov-palette.png
  10. 1 1
      bin/endtoend-build-tests
  11. 2 2
      bin/to-gif.sh
  12. 2 2
      netlify.toml
  13. 1 0
      packages/@uppy/aws-s3/package.json
  14. 20 7
      packages/@uppy/aws-s3/src/index.js
  15. 5 2
      packages/@uppy/companion/docker-compose-dev.yml
  16. 4 1
      packages/@uppy/companion/docker-compose.yml
  17. 23 0
      packages/@uppy/companion/env_example
  18. 21 22
      packages/@uppy/companion/package.json
  19. 0 16
      packages/@uppy/core/src/_utils.scss
  20. 29 16
      packages/@uppy/core/src/index.js
  21. 72 0
      packages/@uppy/core/src/index.test.js
  22. 6 5
      packages/@uppy/dashboard/src/components/PanelTopBar.js
  23. 8 8
      packages/@uppy/dashboard/src/index.js
  24. 2 2
      packages/@uppy/dashboard/src/style.scss
  25. 2 1
      packages/@uppy/status-bar/src/StatusBar.js
  26. 16 9
      packages/@uppy/status-bar/src/index.js
  27. 64 16
      packages/@uppy/thumbnail-generator/src/index.js
  28. 81 19
      packages/@uppy/thumbnail-generator/src/index.test.js
  29. 1 1
      packages/@uppy/transloadit/src/AssemblyOptions.js
  30. 11 2
      packages/@uppy/transloadit/src/index.js
  31. 3 0
      packages/@uppy/utils/src/getFileTypeExtension.js
  32. 3 0
      packages/@uppy/utils/src/getFileTypeExtension.test.js
  33. 1 1
      packages/@uppy/utils/src/isPreviewSupported.js
  34. 1 1
      packages/@uppy/utils/src/isPreviewSupported.test.js
  35. 0 7
      packages/@uppy/xhr-upload/src/index.js
  36. 0 5
      test/endtoend/thumbnails/test.js
  37. 23 0
      test/endtoend/transloadit/index.html
  38. 51 0
      test/endtoend/transloadit/main.js
  39. 55 0
      test/endtoend/transloadit/test.js
  40. 22 0
      test/endtoend/url-plugin/disabled.test.js
  41. 28 0
      test/endtoend/url-plugin/index.html
  42. 17 0
      test/endtoend/url-plugin/main.js
  43. 25 4
      test/endtoend/utils.js
  44. 2 0
      test/endtoend/wdio.base.conf.js
  45. 3 3
      website/_config.yml
  46. 2 2
      website/src/_data/design_goals.yml
  47. 9 1
      website/src/api-usage-example.js
  48. 6 0
      website/src/docs/aws-s3.md
  49. 2 0
      website/src/docs/dashboard.md
  50. 1 1
      website/src/docs/react.md
  51. 21 0
      website/src/docs/transloadit.md
  52. 59 0
      website/src/docs/uppy.md
  53. 5 1
      website/src/examples/transloadit/app.es6
  54. 1 1
      website/src/frontpage-code-sample.ejs
  55. binární
      website/src/images/uppy-demo-oct-2018.mp4
  56. 22 14
      website/themes/uppy/layout/index.ejs
  57. 10 2
      website/themes/uppy/layout/partials/frontpage-code-sample.html
  58. 2 2
      website/themes/uppy/source/css/_index.scss

+ 10 - 20
.travis.yml

@@ -1,28 +1,22 @@
 language: node_js
 node_js:
 - 8.11.4
-
 before_install:
-  - nvm install-latest-npm
-
+- nvm install-latest-npm
 install:
-  - npm install
-  - npm run bootstrap -- --no-ci
-
+- npm install
+- npm run bootstrap -- --no-ci
 script:
-  - npm run build
-  - npm run test
-  - 'if [ "${TRAVIS_PULL_REQUEST}" = "false" ]; then npm run test:acceptance; fi'
-  - npm run uploadcdn
-
+- npm run build
+- npm run test
+- if [ "${TRAVIS_PULL_REQUEST}" = "false" ]; then npm run test:acceptance; fi
+- npm run uploadcdn
 cache:
   apt: true
   directories:
-  - ~/.npm
-
+  - "~/.npm"
 services:
-  - docker
-
+- docker
 addons:
   apt:
     sources:
@@ -34,7 +28,6 @@ addons:
       secure: nAMJ/d1fm9urTYsQ+1uqj6Jjf71J8rzwYBSZbTDAeUEZzAdvGc0a9H3PYWM4pnUDPo5s1c9MMetXi2XNdUbXgMKHbEnePZ2mJamqFtXMmpG8pgFmMqj+btMd7Yybt070tRsn4Vy0uBSi2H/en7F3j+grABJV+SAXqWkSB7CU1fZaN/u0DpoGBNj1ZNwkYCIhpLueYJTPRWBOodMAarXuFv5+7KFOKuZM3tF/JjsMNSSaDgTkz13BZnbX6vNPxGJJNJcyJGSaXrVW8hh1Zmvnk/XdiLy+vt7Wz1wz3A9ebiFDuydo5AAkxrLFsCJ5nGEqLg3bkr6NaTRpbM84ZT3i1FQMTdKP6OHHqwAeBscB6BkyhZhzvsFtl2YRBNK9mA3OtOYvBmTkFkNqvrPQlfu7cFtyG5+AUfSCiTTgS/vWIwoqSVAXaOEqN8Fp54ecUdkzCTttl3gXteZzNLRYvyQcFpoJb6E+dS8qAW0OFOteiwKVuPCh3nGUzBP13bRo1i9UAX7ZCTlpjinkxE8ryzbToo6ZcVQMBAkKhaw/x8GzOtfm5rgYMeQzGEoBJNfr7qqfs7JMxAIEMYjrTL9PXVOp/R8F3FdsqbV70jSyfsxMSMkwSWFRmVslG8+Djy8P3LnckGy1FEbMHnH8GZHZg+hbBzN8Be1/1fV0oRRAr939WRc=
     access_key:
       secure: OY3oWwiJghfty9wSPVvlhirvFGxPHDdIRuVkzAv6j7C/hj2BWYAP/UHrwdQ9XiYisHi/B5mGeyRVlrAf0MNGrG84rTDUbTWZbmktfuxl7A+Y6c0czk+s4SdhOiANG5b3tFl5wKq8h7uhrWH5/jWoKQ2Fz1VDCqxTvvZQbo41jSBhi7TBia626hxEePzdaiuw6HhGFZtfaoVs/FX30ylz8WDNrBjwCynjxsT52BaQrVvgEhuyzlOpI69YkZBPOq4fc3KiZ2YR43gLTx8K+sYCE9yJxdg1xT/UAawEhmedU83nyBZVo4rr7+03AixIxtI28MUCfBMlcsGwBxcKEKY/IWcp9UkPCq6+zALQoncV478tP21eYvlmxSFhYCrv+WEQlN+BcNjr4OJlmmFDbCVaF7r9qLeQPImU0+9iJU3OjrW7lpfLxORpGDEr2Nx6awKkIJCxNyK9weefeNo6Fz3V1kkyZ/7yWFeniJnRUCbahrB2XgzxIE+W307s1Qs4fm6JK7hVLTtG4fBzjChmAyGIzu744ws9WqmjvkC9D7OfnuXqanv/VcBFqPiudInerv7NL8FketUC+fxe/7XJfcxdaDGBjk8Kq7zXDohGRGymUXEoMDNJsKkMMlaKzdf7tgqdhsRJoH9NCVqrDXuG5al0UtrDP5RS7qfoxUunJmNFhlg=
-
 env:
   global:
   - CXX=g++-4.8
@@ -60,13 +53,10 @@ env:
   - secure: V6qVB6vHAuSt1YPgzYpknLglnxbYJM5bsjAjQi78zZQSqrrWCMBgp5Q+Sq7Ad8nycgnPl8W8VpMnUIBqZcK4ciCW+vJBtZ7fBJLIEGMEk8+c8az8JePFcBRiaEicZPvZFuIHPteaAy6jK1GRVpjSjJi3NYEMpcDUs2aTM2bIpX0ks2WjUqTXiK8EvIaK2LTOgSBvyGUOiHwwTkom+PyD50t7jP98g+9Pn7GPwtWaUo1K8UpLArwR2fZ/nm0F2DU6ohz7o2t4LATFXuZhbOCj7O9vh+CmBf+/0C7giT3K23LbckDSK8CNTCDC2bsv8zDo5ZZtgdOvdatOD+VlsbAMkmUzzDgQ3410arZ8g1LVQw8eJwu1H+etcAinYf4r+Tr7s3dTBliMX70/IpjZrkHR9uXEi4LelBNYn6TKdLTW3lfVWkiFs/HPu4SoAmu1+sxSvwxVDDLeexQmb2fwp0r2tDKZ3eRfpQXLPACBaORnGW6ki9qXo8PBdaElxPbpzEWzAfT3F6zRjebpow+cdFBQrAga7/LrOrXerZxH2ekRWKgXzuhw7ERwVFrVN4/NlmDJXo+rm4na7n7CxY/QpZcdpthksDO9KYCqmn5dqMqFGislRvRe7PqIti8gC4pSdeUhDKsIh9OEgVNjCczvnA8rMRQ5aNmFx6wspmrBvO7qRLM=
   - secure: rVsiFPA9TvH/d2wkP8+1i5QGUuYw0q2BUAUdxyxO9hQcG/nRiHXtQfLbTRZHKwvqf0vyV6J1pJqLlVN4JO/bPhAvk55KAQJWl8UqyaeZiEN9KMcTr3fJuNFlBj4ciYiZ3BWwakblsiaGCjKMRdjki58a9f8XL2rcM8R6ccndjkTMYnBKaopSsAgouI8D5n74wQz6lODUayGOlbwlGLfGtPYplUfSLK4wghC+jgWsNjJySqJhfgYS0JCZc10Qw+FI2BoU4SZ4+P0L0YPIC9zV/cUW4qDT11N/oUgwfjZbPWfM9A/xn/d7sgDH+SpeoGdYler/lvxojj6L2mD/wAh8/lg1E6nL2aKgExE3z+fd2XV8L0osB/sulB7/Exrezg/mVejAx2IkWVHi4VEJmcTV+3WeEvTFOM3fID8dOVf+GUv+hcHdZMxS/hfj3keKCYG1P5ameMJO8FehRqhetNYnr6FTyrK+S+xitaZ/nXrTbHItPS0pZ4XA6CFs5uzMBPeDnk5/D7paPyrE/k2HAc1WmA6g37OyzYIMEV1laBz8IG0qMqg6JJmr09P/Iwrim5Ex2fAssT9Yr1WuOE2gWoF0A3XuVXQHVf4tJT6x/WDKChmbX588a47AvBgkFyoXLRilUYlET2tWnEpVxUovsbJXqvHwTXWMLO9riRjjeInbpvA=
   - secure: eq5hOqRBN2R7YO2dYdn5OjZc/zLLYYDZcCpCu/K/8fU4HYWTqxrBntjv8T0sZ5qdlAs3IniEfXxemz9V3zwvxR+vh2bGuYr2Xo7RRa2TIDuw+KUPZogrVxhXHPKfyJqstxy+dee2+pWhGkAP7caiu49eyqlboBMkzgpO/xcdehEWYRY5jPgvnlH+QRZ3GADKs1JEeltHDiZ6rYA7nj5Tyx9UoLgv4Av9UXdC29we7dLFTkVfCHE//7wfZW9+/IbxthA4qMjQOFaBrmagN5yweDg87iPTqNMth7FjzOavdUgQ2TW6d10VDEhLIZh36gLGreViKMDCEWKMQ8f/mv05Ao8+DXyXgxIn56II8lhUp5ukQ27ZWixfEKFx2lynJWRZE0pWwf8ec1+bXLQiBOE181Cl4nUT/TbFWzvV6yA+cMiQKe4y59bC4nhkK3IYgpR5kfCFOT+1tFknQ4hNJNacWwUmaDFMxYJaXEtRUn5jJa7eGRYSCrmnymbnzZ6w3Q3nQGNvNxpbBIXX/pzs0VDVTxSlgN4gA+n2jeCyjgVVrMQ/HoAS4uwm1cx89AttW+TANppg1PqWhhrJYuVEZSnvV8PM6R7rbvlS5tluezQj41YklgjsSopH7//+dbGGDNbrTTLic4J9PJR3yEtlAMdOCi53iT0R0Dt5X2WBv2QT5Eg=
-
+  - secure: ZOSwtE0lBuVO8ncxedW3yGRactSMy+QH/ySPBy79eRmuqrNR+c4mfhiYQAyJ9jmw746YT+8+X/eRwlKOa0SDzUOxEdq6lHqUjubaLvLvy7mr5WHR2b2HDmVYUNNvKgLxn9QPgNwja1toSq/gtK/oi69ENbQLMOHgP8anOK4P6UK4NvoMu1DjyPmBStMAWeEMwuwAaYf4RgAkCsOLTJBejL8kCtWSZNBJmT05lBbE1oSFy77Tg4h93PESOOODek2RRgIikR7LK2rI4tG2oHeo5uFnCe83yu7ZTEI3ArxYtqynYn8iy/zOHf3ewIwgqcqRV/EcvKKwCHx7yi/VNk3qKoz5/jWzE9+ceoUd3vXaU7r1SfXX8ACTmQArZqzU7Rng2s+7r8I8//XCGAsGI6/cOOlS7I7Yr7Ack6CqLlL2HBIoNGYT+jXP/VY9pgkm6YlpNAphdoQrZ1vm/0CtQQ/q5H0fW16qgUJZ1vCk+3PZYNt//83bI7TyIwX72dCrMKzub6bhQtWRMSCfxZELFgue8f+cQf/+NmHhmd9yRJyVhiZvtE/4RN4F32dJbJjtOb4HjFTQIAyE2rZlB4tjOsTKJK7cT+X4enRzyinDYpAjMgNwloAg8xsn+YE53pZJLFUqk8q1yUE6XkSK9fYRSKVc3mb4ICDY4/hTzJRBf+tSUSI=
 notifications:
   slack:
     secure: L3iQQE8sZ0ik1Z26gPoNMiIam9EOEwYhraHCY60Jk/wmfH6SW/727yKXpgcb/yayx37rUZplvoO7H8e05ISxTJKSepEeqbBUIBQs48S8hr+FHk0VPtpP4HGxqaITRLm+mI1coPRvfISxzrB8d240oup6muhC9Ws4/LXi6v8miyIOs2zoYmGxd56TrUeON3UYlKt6dMava0V4bugARzrafN/tfyI9ccqbHzQLBspQvBI61DzZ5I2vnWpkjfWgIHz9Fl4VzXHqMXwjuTUEu8ibA12b3dHZiJEAoqeb9Oj9QcLPbstPLhlNTZZaOrfiFtwLctI2rFh37slDpAfk5idv3ycxcoG5rbCxgyg5i6dpQqrqHxnyglgHg2/nZ+YA5okeS7nJJNtU/4S6AFRWOUUWMVVY0VBEV+8w+uurl0PDy80RUY3uyK64qAgQ8U0M81/Ys1oyWyn78TqHcbby7V2Ws5I9Yakrq8D+mdfsWYCio8F6LXHSwJ0mt2FanJtdDvpPk9sAwsXZN0n8xhELt5TiRp3bzVIQ0IPUgF54dTG9/zWRvC1P4TFaFU/2fg73ZEUC5aWJoFMnLSZjbZvp5gwpCVd0MjSBk80nF9dHYcavIgJ0wMGI3BMb8Nn6+T11Gw/ycr7OGU4NMkj7i8vSFgKF74piWZyiNW8orkMN6XZgM+o=
-
-# Travis docs: Note that pull request builds skip deployment step altogether.
-# https://docs.travis-ci.com/user/deployment/#Conditional-Releases-with-on
 deploy:
 - provider: script
   skip_cleanup: true

+ 5 - 0
CHANGELOG.md

@@ -73,6 +73,7 @@ PRs are welcome! Please do open an issue to discuss first if it's a big feature,
 - [ ] test: Add a prepublish test that checks if `npm pack` is not massive
 - [ ] Add release documentation. eg: test on transloadit website, check examples on the uppy.io website
 - [ ] dashboard: add image cropping, study https://github.com/MattKetmo/darkroomjs/, https://github.com/fengyuanchen/cropperjs #151
+- [ ] webcam: Pick format based on `restrictions.allowedFileTypes`, eg. use PNG for snapshot instead of JPG if `allowedFileTypes: ['.png']` is set
 
 ## 1.0 Goals
 
@@ -129,6 +130,10 @@ What we need to do to release Uppy 1.0
 - [ ] companion: option validation (can use https://npm.im/ajv + JSON schema)
 - [ ] transloadit: add end2end test for transloadit https://uppy.io/examples/transloadit/ (@arturi, @goto-bus-stop)
 - [ ] companion: rename `serverUrl` and `serverPattern` to `companionUrl` and `companionAllowedHosts` (@ifedapoolarewaju)
+- [x] @uppy/aws-s3: Use RequestClient (#1091 / @goto-bus-stop)
+- [x] @uppy/transloadit: Add `COMPANION_PATTERN` constant (#1104 / @goto-bus-stop)
+- [x] @uppy/core: Add `allowMultipleUploads` option (#1064 / @goto-bus-stop)
+- [x] @uppy/webcam: Fix getting data from Webcam recording if mime type includes codec metadata (#1094 / @goto-bus-stop)
 
 ## 0.27.5
 

+ 4 - 4
README.md

@@ -11,16 +11,16 @@ Uppy is a sleek, modular JavaScript file uploader that integrates seamlessly wit
 - **Preview** and edit metadata with a nice interface;
 - **Upload** to the final destination, optionally process/encode
 
-<img src="https://github.com/transloadit/uppy/raw/master/assets/uppy-demo-2.gif">
+<img src="https://github.com/transloadit/uppy/raw/master/assets/uppy-demo-oct-2018.gif">
 
 **[Read the docs](https://uppy.io/docs)** | **[Try Uppy](https://uppy.io/examples/dashboard/)**
 
+<a href="https://transloadit.com" target="_blank"><img width="185" src="https://github.com/transloadit/uppy/raw/master/assets/developed-by-transloadit.png"></a>
+
 Uppy is being developed by the folks at [Transloadit](https://transloadit.com), a versatile file encoding service.
 
 ## 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">
-
 Code used in the above example:
 
 ```js
@@ -176,7 +176,7 @@ Not all apps need all of these features. A `<input type="file">` is fine in many
 
 ### Why is all this goodness free?
 
-Transloadit’s team is small and we have a shared ambition to make a living from open source. By giving away projects like [tus.io](https://tus.io) and [Uppy](https://uppy.io),we’re hoping to advance the state of the art, make life a tiny little bit better for everyone, and in doing so have rewarding jobs and get some eyes on our commercial service: [a content ingestion & processing platform](https://transloadit.com).
+Transloadit’s team is small and we have a shared ambition to make a living from open source. By giving away projects like [tus.io](https://tus.io) and [Uppy](https://uppy.io), we’re hoping to advance the state of the art, make life a tiny little bit better for everyone, and in doing so have rewarding jobs and get some eyes on our commercial service: [a content ingestion & processing platform](https://transloadit.com).
 
 Our thinking is that if just a fraction of our open source userbase can see the appeal of hosted versions straight from the source, that could already be enough to sustain our work. So far this is working out! We’re able to dedicate 80% of our time to open source and haven’t gone bankrupt just yet :D
 

binární
assets/developed-by-transloadit.png


binární
assets/uppy-demo-oct-2018.gif


binární
assets/uppy-demo-oct-2018.mov


binární
assets/uppy-demo-oct-2018.mov-palette.png


binární
assets/uppy-demo-oct-2018.mp4


binární
assets/uppy-demo2.mov-palette.png


+ 1 - 1
bin/endtoend-build-tests

@@ -12,7 +12,7 @@ __base="$(basename ${__file} .sh)"
 __root="$(cd "$(dirname "${__dir}")" && pwd)"
 
 # Tests using a simple build setup.
-tests="tus-drag-drop tus-dashboard i18n-drag-drop xhr-limit providers thumbnails"
+tests="tus-drag-drop tus-dashboard i18n-drag-drop xhr-limit providers thumbnails transloadit url-plugin"
 
 for t in $tests; do
   mkdir -p "${__root}/test/endtoend/$t/dist"

+ 2 - 2
bin/to-gif.sh

@@ -12,7 +12,7 @@ __root="$(cd "$(dirname "${__dir}")" && pwd)"
 
 width=600
 speed=0.7
-input="${__root}/assets/uppy-demo2.mov"
+input="${__root}/assets/uppy-demo-oct-2018.mov"
 base="$(basename "${input}")"
 output="${__root}/assets/${base}.gif"
 
@@ -29,4 +29,4 @@ ffmpeg \
   "${output}"
 
 du -hs "${output}"
-open -a 'Google Chrome' "${output}"
+open -a 'Google Chrome' "${output}"

+ 2 - 2
netlify.toml

@@ -6,6 +6,6 @@ ID = "uppy"
 
 [context.deploy-preview]
 # netlify caches node_modules and doesn't run `npm install` after the first time,
-# so we have to `run prepare` manually in order to get lerna to do its thing
-command = "npm run prepare && npm run build && npm run web:install && npm run web:disc && npm run web:build"
+# so we have to `run bootstrap` manually in order to get lerna to do its thing
+command = "npm run bootstrap && npm run build && npm run web:install && npm run web:disc && npm run web:build"
 publish = "website/public"

+ 1 - 0
packages/@uppy/aws-s3/package.json

@@ -25,6 +25,7 @@
   "dependencies": {
     "@uppy/utils": "0.27.1",
     "@uppy/xhr-upload": "0.27.4",
+    "@uppy/companion-client": "0.27.2",
     "resolve-url": "^0.2.1"
   },
   "devDependencies": {

+ 20 - 7
packages/@uppy/aws-s3/src/index.js

@@ -2,6 +2,7 @@ const resolveUrl = require('resolve-url')
 const { Plugin } = require('@uppy/core')
 const Translator = require('@uppy/utils/lib/Translator')
 const limitPromises = require('@uppy/utils/lib/limitPromises')
+const { RequestClient } = require('@uppy/companion-client')
 const XHRUpload = require('@uppy/xhr-upload')
 
 function isXml (xhr) {
@@ -9,6 +10,15 @@ function isXml (xhr) {
   return typeof contentType === 'string' && contentType.toLowerCase() === 'application/xml'
 }
 
+function assertServerError (res) {
+  if (res && res.error) {
+    const error = new Error(res.message)
+    Object.assign(error, res.error)
+    throw error
+  }
+  return res
+}
+
 module.exports = class AwsS3 extends Plugin {
   constructor (uppy, opts) {
     super(uppy, opts)
@@ -29,13 +39,18 @@ module.exports = class AwsS3 extends Plugin {
       locale: defaultLocale
     }
 
-    this.opts = Object.assign({}, defaultOptions, opts)
-    this.locale = Object.assign({}, defaultLocale, this.opts.locale)
-    this.locale.strings = Object.assign({}, defaultLocale.strings, this.opts.locale.strings)
+    this.opts = { ...defaultOptions, ...opts }
+    this.locale = {
+      ...defaultLocale,
+      ...this.opts.locale,
+      strings: { ...defaultLocale.strings, ...this.opts.locale.strings }
+    }
 
     this.translator = new Translator({ locale: this.locale })
     this.i18n = this.translator.translate.bind(this.translator)
 
+    this.client = new RequestClient(uppy, opts)
+
     this.prepareUpload = this.prepareUpload.bind(this)
 
     if (typeof this.opts.limit === 'number' && this.opts.limit !== 0) {
@@ -52,10 +67,8 @@ module.exports = class AwsS3 extends Plugin {
 
     const filename = encodeURIComponent(file.name)
     const type = encodeURIComponent(file.type)
-    return fetch(`${this.opts.serverUrl}/s3/params?filename=${filename}&type=${type}`, {
-      method: 'get',
-      headers: { accept: 'application/json' }
-    }).then((response) => response.json())
+    return this.client.get(`s3/params?filename=${filename}&type=${type}`)
+      .then(assertServerError)
   }
 
   validateParameters (file, params) {

+ 5 - 2
packages/@uppy/companion/docker-compose-dev.yml

@@ -2,7 +2,7 @@ version: '2'
 
 services:
   uppy:
-    image: companion
+    image: transloadit/companion
     build:
       context: .
       dockerfile: Dockerfile
@@ -11,6 +11,9 @@ services:
     volumes:
       - ./:/app
       - /app/node_modules
+      - /mnt/uppy-server-data:/mnt/uppy-server-data
     ports:
       - "3020:3020"
-    command: ["/usr/bin/nodemon", "/app/lib/standalone/start-server.js", "--config", "/app/nodemon.json"]
+    command: "/app/src/standalone/start-server.js --config nodemon.json"
+    env_file:
+     - .env

+ 4 - 1
packages/@uppy/companion/docker-compose.yml

@@ -2,11 +2,14 @@ version: '2'
 
 services:
   uppy:
-    image: companion
+    image: transloadit/companion
     build:
       context: .
       dockerfile: Dockerfile
     volumes:
       - /app/node_modules
+      - /mnt/uppy-server-data:/mnt/uppy-server-data
     ports:
       - "3020:3020"
+    env_file:
+     - .env

+ 23 - 0
packages/@uppy/companion/env_example

@@ -0,0 +1,23 @@
+NODE_ENV=dev
+COMPANION_PORT=3020
+COMPANION_DOMAIN=uppy.xxxx.com
+COMPANION_SELF_ENDPOINT=uppy.xxxx.com
+
+COMPANION_PROTOCOL=https
+COMPANION_DATADIR=/mnt/uppy-server-data
+COMPANION_SECRET=
+
+COMPANION_DROPBOX_KEY="dropbox_key"
+COMPANION_DROPBOX_SECRET="dropbox_secret"
+
+COMPANION_GOOGLE_KEY=
+COMPANION_GOOGLE_SECRET=
+
+COMPANION_INSTAGRAM_KEY=
+COMPANION_INSTAGRAM_SECRET=
+
+COMPANION_AWS_KEY=
+COMPANION_AWS_SECRET=
+COMPANION_AWS_BUCKET=
+COMPANION_AWS_ENDPOINT=
+COMPANION_AWS_REGION=

+ 21 - 22
packages/@uppy/companion/package.json

@@ -28,27 +28,6 @@
   "bin": {
     "companion": "./lib/standalone/start-server.js"
   },
-  "devDependencies": {
-    "@types/aws-serverless-express": "^3.0.1",
-    "@types/compression": "0.0.36",
-    "@types/connect-redis": "0.0.7",
-    "@types/cookie-parser": "^1.4.1",
-    "@types/cors": "^2.8.3",
-    "@types/express-session": "^1.15.6",
-    "@types/helmet": "0.0.37",
-    "@types/jsonwebtoken": "^7.2.5",
-    "@types/lodash.merge": "^4.6.3",
-    "@types/morgan": "^1.7.35",
-    "@types/ms": "^0.7.30",
-    "@types/node": "^8.5.1",
-    "@types/request": "^2.0.9",
-    "@types/tus-js-client": "^1.4.1",
-    "@types/uuid": "^3.4.3",
-    "@types/ws": "^3.2.1",
-    "nodemon": "^1.17.5",
-    "supertest": "3.0.0",
-    "typescript": "^2.9.2"
-  },
   "dependencies": {
     "@purest/providers": "1.0.0",
     "@uppy/fs-tail-stream": "^1.2.0",
@@ -77,11 +56,31 @@
     "request": "2.85.0",
     "serialize-error": "^2.1.0",
     "tus-js-client": "^1.5.1",
-    "typescript": "^2.9.2",
     "uuid": "2.0.2",
     "validator": "^9.0.0",
     "ws": "1.1.5"
   },
+  "devDependencies": {
+    "@types/aws-serverless-express": "^3.0.1",
+    "@types/compression": "0.0.36",
+    "@types/connect-redis": "0.0.7",
+    "@types/cookie-parser": "^1.4.1",
+    "@types/cors": "^2.8.3",
+    "@types/express-session": "^1.15.6",
+    "@types/helmet": "0.0.37",
+    "@types/jsonwebtoken": "^7.2.5",
+    "@types/lodash.merge": "^4.6.3",
+    "@types/morgan": "^1.7.35",
+    "@types/ms": "^0.7.30",
+    "@types/node": "^8.5.1",
+    "@types/request": "^2.0.9",
+    "@types/tus-js-client": "^1.4.1",
+    "@types/uuid": "^3.4.3",
+    "@types/ws": "^3.2.1",
+    "nodemon": "^1.17.5",
+    "supertest": "3.0.0",
+    "typescript": "^2.9.2"
+  },
   "files": [
     "lib/"
   ],

+ 0 - 16
packages/@uppy/core/src/_utils.scss

@@ -23,19 +23,3 @@
   border: 0;
   color: inherit;
 }
-
-// Animations
-
-@keyframes fadeIn {
-  0% {
-    opacity: 0;
-  }
-
-  25% {
-    opacity: 1;
-  }
-
-  100% {
-    opacity: 0;
-  }
-}

+ 29 - 16
packages/@uppy/core/src/index.js

@@ -8,7 +8,6 @@ const DefaultStore = require('@uppy/store-default')
 const getFileType = require('@uppy/utils/lib/getFileType')
 const getFileNameAndExtension = require('@uppy/utils/lib/getFileNameAndExtension')
 const generateFileID = require('@uppy/utils/lib/generateFileID')
-const isObjectURL = require('@uppy/utils/lib/isObjectURL')
 const getTimeStamp = require('@uppy/utils/lib/getTimeStamp')
 const Plugin = require('./Plugin') // Exported from here.
 
@@ -54,6 +53,7 @@ class Uppy {
     const defaultOptions = {
       id: 'uppy',
       autoProceed: false,
+      allowMultipleUploads: true,
       debug: false,
       restrictions: {
         maxFileSize: null,
@@ -118,11 +118,12 @@ class Uppy {
       plugins: {},
       files: {},
       currentUploads: {},
+      allowNewUpload: true,
       capabilities: {
         resumableUploads: false
       },
       totalProgress: 0,
-      meta: Object.assign({}, this.opts.meta),
+      meta: { ...this.opts.meta },
       info: {
         isHidden: true,
         type: 'info',
@@ -378,7 +379,7 @@ class Uppy {
   * @param {object} file object to add
   */
   addFile (file) {
-    const { files } = this.getState()
+    const { files, allowNewUpload } = this.getState()
 
     const onError = (msg) => {
       const err = typeof msg === 'object' ? msg : new Error(msg)
@@ -387,6 +388,10 @@ class Uppy {
       throw err
     }
 
+    if (allowNewUpload === false) {
+      onError(new Error('Cannot add new files: already uploading.'))
+    }
+
     const onBeforeFileAddedResult = this.opts.onBeforeFileAdded(file, files)
 
     if (onBeforeFileAddedResult === false) {
@@ -500,15 +505,13 @@ class Uppy {
     this._calculateTotalProgress()
     this.emit('file-removed', removedFile)
     this.log(`File removed: ${removedFile.id}`)
-
-    // Clean up object URLs.
-    if (removedFile.preview && isObjectURL(removedFile.preview)) {
-      URL.revokeObjectURL(removedFile.preview)
-    }
   }
 
   pauseResume (fileID) {
-    if (this.getFile(fileID).uploadComplete) return
+    if (!this.getState().capabilities.resumableUploads ||
+         this.getFile(fileID).uploadComplete) {
+      return
+    }
 
     const wasPaused = this.getFile(fileID).isPaused || false
     const isPaused = !wasPaused
@@ -592,6 +595,7 @@ class Uppy {
     })
 
     this.setState({
+      allowNewUpload: true,
       totalProgress: 0,
       error: null
     })
@@ -853,14 +857,13 @@ class Uppy {
   /**
    * Find one Plugin by name.
    *
-   * @param {string} name description
+   * @param {string} id plugin id
    * @return {object | boolean}
    */
-  getPlugin (name) {
+  getPlugin (id) {
     let foundPlugin = null
     this.iteratePlugins((plugin) => {
-      const pluginName = plugin.id
-      if (pluginName === name) {
+      if (plugin.id === id) {
         foundPlugin = plugin
         return false
       }
@@ -1025,6 +1028,11 @@ class Uppy {
    * @return {string} ID of this upload.
    */
   _createUpload (fileIDs) {
+    const { allowNewUpload, currentUploads } = this.getState()
+    if (!allowNewUpload) {
+      throw new Error('Cannot create a new upload: already uploading.')
+    }
+
     const uploadID = cuid()
 
     this.emit('upload', {
@@ -1033,20 +1041,25 @@ class Uppy {
     })
 
     this.setState({
-      currentUploads: Object.assign({}, this.getState().currentUploads, {
+      allowNewUpload: this.opts.allowMultipleUploads !== false,
+
+      currentUploads: {
+        ...currentUploads,
         [uploadID]: {
           fileIDs: fileIDs,
           step: 0,
           result: {}
         }
-      })
+      }
     })
 
     return uploadID
   }
 
   _getUpload (uploadID) {
-    return this.getState().currentUploads[uploadID]
+    const { currentUploads } = this.getState()
+
+    return currentUploads[uploadID]
   }
 
   /**

+ 72 - 0
packages/@uppy/core/src/index.test.js

@@ -152,6 +152,7 @@ describe('src/Core', () => {
         capabilities: { resumableUploads: false },
         files: {},
         currentUploads: {},
+        allowNewUpload: true,
         foo: 'baar',
         info: { isHidden: true, message: '', type: 'info' },
         meta: {},
@@ -175,6 +176,7 @@ describe('src/Core', () => {
         capabilities: { resumableUploads: false },
         files: {},
         currentUploads: {},
+        allowNewUpload: true,
         foo: 'bar',
         info: { isHidden: true, message: '', type: 'info' },
         meta: {},
@@ -187,6 +189,7 @@ describe('src/Core', () => {
         capabilities: { resumableUploads: false },
         files: {},
         currentUploads: {},
+        allowNewUpload: true,
         foo: 'baar',
         info: { isHidden: true, message: '', type: 'info' },
         meta: {},
@@ -204,6 +207,7 @@ describe('src/Core', () => {
         capabilities: { resumableUploads: false },
         files: {},
         currentUploads: {},
+        allowNewUpload: true,
         foo: 'bar',
         info: { isHidden: true, message: '', type: 'info' },
         meta: {},
@@ -231,6 +235,7 @@ describe('src/Core', () => {
       capabilities: { resumableUploads: false },
       files: {},
       currentUploads: {},
+      allowNewUpload: true,
       error: null,
       foo: 'bar',
       info: { isHidden: true, message: '', type: 'info' },
@@ -289,6 +294,7 @@ describe('src/Core', () => {
       capabilities: { resumableUploads: false },
       files: {},
       currentUploads: {},
+      allowNewUpload: true,
       error: null,
       info: { isHidden: true, message: '', type: 'info' },
       meta: {},
@@ -641,6 +647,29 @@ describe('src/Core', () => {
       })
       expect(core.getFiles().length).toEqual(0)
     })
+
+    it('allows no new files after upload when allowMultipleUploads: false', async () => {
+      const core = new Core({ allowMultipleUploads: false })
+      core.addFile({
+        source: 'jest',
+        name: 'foo.jpg',
+        type: 'image/jpeg',
+        data: new File([sampleImage], { type: 'image/jpeg' })
+      })
+
+      await core.upload()
+
+      expect(() => {
+        core.addFile({
+          source: 'jest',
+          name: '123.foo',
+          type: 'image/jpeg',
+          data: new File([sampleImage], { type: 'image/jpeg' })
+        })
+      }).toThrow(
+        /Cannot add new files: already uploading\./
+      )
+    })
   })
 
   describe('uploading a file', () => {
@@ -736,6 +765,49 @@ describe('src/Core', () => {
         expect(err).toMatchObject(new Error('Not starting the upload because onBeforeUpload returned false'))
       })
     })
+
+    it('only allows a single upload() batch when allowMultipleUploads: false', async () => {
+      const core = new Core({ allowMultipleUploads: false })
+      core.addFile({
+        source: 'jest',
+        name: 'foo.jpg',
+        type: 'image/jpeg',
+        data: new File([sampleImage], { type: 'image/jpeg' })
+      })
+      core.addFile({
+        source: 'jest',
+        name: 'bar.jpg',
+        type: 'image/jpeg',
+        data: new File([sampleImage], { type: 'image/jpeg' })
+      })
+
+      await expect(core.upload()).resolves.toBeDefined()
+      await expect(core.upload()).rejects.toThrow(
+        /Cannot create a new upload: already uploading\./
+      )
+    })
+
+    it('allows new files again with allowMultipleUploads: false after reset() was called', async () => {
+      const core = new Core({ allowMultipleUploads: false })
+
+      core.addFile({
+        source: 'jest',
+        name: 'bar.jpg',
+        type: 'image/jpeg',
+        data: new File([sampleImage], { type: 'image/jpeg' })
+      })
+      await expect(core.upload()).resolves.toBeDefined()
+
+      core.reset()
+
+      core.addFile({
+        source: 'jest',
+        name: '123.foo',
+        type: 'image/jpeg',
+        data: new File([sampleImage], { type: 'image/jpeg' })
+      })
+      await expect(core.upload()).resolves.toBeDefined()
+    })
   })
 
   describe('removing a file', () => {

+ 6 - 5
packages/@uppy/dashboard/src/components/PanelTopBar.js

@@ -67,9 +67,11 @@ function UploadStatus (props) {
 }
 
 function PanelTopBar (props) {
-  const notOverFileLimit = props.maxNumberOfFiles
-    ? props.totalFileCount < props.maxNumberOfFiles
-    : true
+  let allowNewUpload = props.allowNewUpload
+  // TODO maybe this should be done in ../index.js, then just pass that down as `allowNewUpload`
+  if (allowNewUpload && props.maxNumberOfFiles) {
+    allowNewUpload = props.totalFileCount < props.maxNumberOfFiles
+  }
 
   return (
     <div class="uppy-DashboardContent-bar">
@@ -84,7 +86,7 @@ function PanelTopBar (props) {
       <div class="uppy-DashboardContent-title" role="heading" aria-level="h1">
         <UploadStatus {...props} />
       </div>
-      { notOverFileLimit &&
+      { allowNewUpload &&
         <button class="uppy-DashboardContent-addMore"
           type="button"
           aria-label={props.i18n('addMoreFiles')}
@@ -95,7 +97,6 @@ function PanelTopBar (props) {
           </svg>
         </button>
       }
-
     </div>
   )
 }

+ 8 - 8
packages/@uppy/dashboard/src/index.js

@@ -513,7 +513,7 @@ module.exports = class Dashboard extends Plugin {
 
   render (state) {
     const pluginState = this.getPluginState()
-    const { files, capabilities } = state
+    const { files, capabilities, allowNewUpload } = state
 
     const newFiles = Object.keys(files).filter((file) => {
       return !files[file].progress.uploadStarted
@@ -602,7 +602,6 @@ module.exports = class Dashboard extends Plugin {
     }
 
     const cancelUpload = (fileID) => {
-      this.uppy.emit('upload-cancel', fileID)
       this.uppy.removeFile(fileID)
     }
 
@@ -612,9 +611,9 @@ module.exports = class Dashboard extends Plugin {
     }
 
     return DashboardUI({
-      state: state,
+      state,
       modal: pluginState,
-      files: files,
+      files,
       newFiles,
       uploadStartedFiles,
       completeFiles,
@@ -627,7 +626,8 @@ module.exports = class Dashboard extends Plugin {
       isAllPaused,
       totalFileCount: Object.keys(files).length,
       totalProgress: state.totalProgress,
-      acquirers: acquirers,
+      allowNewUpload,
+      acquirers,
       activePanel: pluginState.activePanel,
       animateOpenClose: this.opts.animateOpenClose,
       isClosing: pluginState.isClosing,
@@ -652,16 +652,16 @@ module.exports = class Dashboard extends Plugin {
       metaFields: pluginState.metaFields,
       resumableUploads: capabilities.resumableUploads || false,
       bundled: capabilities.bundled || false,
-      startUpload: startUpload,
+      startUpload,
       pauseUpload: this.uppy.pauseResume,
       retryUpload: this.uppy.retryUpload,
-      cancelUpload: cancelUpload,
+      cancelUpload,
       cancelAll: this.uppy.cancelAll,
       fileCardFor: pluginState.fileCardFor,
       toggleFileCard: this.toggleFileCard,
       toggleAddFilesPanel: this.toggleAddFilesPanel,
       showAddFilesPanel: pluginState.showAddFilesPanel,
-      saveFileCard: saveFileCard,
+      saveFileCard,
       width: this.opts.width,
       height: this.opts.height,
       showLinkToFileUploadResult: this.opts.showLinkToFileUploadResult,

+ 2 - 2
packages/@uppy/dashboard/src/style.scss

@@ -623,7 +623,7 @@
     float: left;
     width: 140px;
     height: 170px;
-    margin: 5px 20px;
+    margin: 5px 15px;
     border: 0;
     background-color: initial;
     border-bottom: none;
@@ -813,7 +813,7 @@
 }
 
 .uppy-DashboardItem-copyLink {
-  width: 11px;
+  width: 12px;
   height: 11px;
 }
 

+ 2 - 1
packages/@uppy/status-bar/src/StatusBar.js

@@ -85,7 +85,8 @@ module.exports = (props) => {
   const width = typeof progressValue === 'number' ? progressValue : 100
   const isHidden = (uploadState === statusBarStates.STATE_WAITING && props.hideUploadButton) ||
     (uploadState === statusBarStates.STATE_WAITING && !props.newFiles > 0) ||
-    (uploadState === statusBarStates.STATE_COMPLETE && props.hideAfterFinish)
+    (uploadState === statusBarStates.STATE_COMPLETE && props.hideAfterFinish) ||
+    !props.allowNewUpload
 
   const progressClassNames = `uppy-StatusBar-progress
                            ${progressMode ? 'is-' + progressMode : ''}`

+ 16 - 9
packages/@uppy/status-bar/src/index.js

@@ -143,7 +143,13 @@ module.exports = class StatusBar extends Plugin {
   }
 
   render (state) {
-    const files = state.files
+    const {
+      capabilities,
+      files,
+      allowNewUpload,
+      totalProgress,
+      error
+    } = state
 
     const uploadStartedFiles = Object.keys(files).filter((file) => {
       return files[file].progress.uploadStarted
@@ -192,7 +198,7 @@ module.exports = class StatusBar extends Plugin {
 
     const isUploadStarted = uploadStartedFiles.length > 0
 
-    const isAllComplete = state.totalProgress === 100 &&
+    const isAllComplete = totalProgress === 100 &&
       completeFiles.length === Object.keys(files).length &&
       processingFiles.length === 0
 
@@ -206,14 +212,15 @@ module.exports = class StatusBar extends Plugin {
 
     const isUploadInProgress = inProgressFiles.length > 0
 
-    const resumableUploads = state.capabilities.resumableUploads || false
+    const resumableUploads = capabilities.resumableUploads || false
 
     return StatusBarUI({
       error: state.error,
       uploadState: this.getUploadingState(isAllErrored, isAllComplete, state.files || {}),
-      totalProgress: state.totalProgress,
-      totalSize: totalSize,
-      totalUploadedSize: totalUploadedSize,
+      allowNewUpload,
+      totalProgress,
+      totalSize,
+      totalUploadedSize,
       isAllComplete,
       isAllPaused,
       isAllErrored,
@@ -222,9 +229,9 @@ module.exports = class StatusBar extends Plugin {
       complete: completeFiles.length,
       newFiles: newFiles.length,
       numUploads: startedFiles.length,
-      totalSpeed: totalSpeed,
-      totalETA: totalETA,
-      files: state.files,
+      totalSpeed,
+      totalETA,
+      files,
       i18n: this.i18n,
       pauseAll: this.uppy.pauseAll,
       resumeAll: this.uppy.resumeAll,

+ 64 - 16
packages/@uppy/thumbnail-generator/src/index.js

@@ -1,10 +1,10 @@
 const { Plugin } = require('@uppy/core')
 const dataURItoBlob = require('@uppy/utils/lib/dataURItoBlob')
+const isObjectURL = require('@uppy/utils/lib/isObjectURL')
 const isPreviewSupported = require('@uppy/utils/lib/isPreviewSupported')
 
 /**
  * The Thumbnail Generator plugin
- *
  */
 
 module.exports = class ThumbnailGenerator extends Plugin {
@@ -15,9 +15,11 @@ module.exports = class ThumbnailGenerator extends Plugin {
     this.title = 'Thumbnail Generator'
     this.queue = []
     this.queueProcessing = false
+    this.defaultThumbnailDimension = 200
 
     const defaultOptions = {
-      thumbnailWidth: 200
+      thumbnailWidth: null,
+      thumbnailHeight: null
     }
 
     this.opts = {
@@ -25,7 +27,8 @@ module.exports = class ThumbnailGenerator extends Plugin {
       ...opts
     }
 
-    this.addToQueue = this.addToQueue.bind(this)
+    this.onFileAdded = this.onFileAdded.bind(this)
+    this.onFileRemoved = this.onFileRemoved.bind(this)
     this.onRestored = this.onRestored.bind(this)
   }
 
@@ -36,7 +39,7 @@ module.exports = class ThumbnailGenerator extends Plugin {
    * @param {number} width
    * @return {Promise}
    */
-  createThumbnail (file, targetWidth) {
+  createThumbnail (file, targetWidth, targetHeight) {
     const originalUrl = URL.createObjectURL(file.data)
 
     const onload = new Promise((resolve, reject) => {
@@ -54,8 +57,8 @@ module.exports = class ThumbnailGenerator extends Plugin {
 
     return onload
       .then(image => {
-        const targetHeight = this.getProportionalHeight(image, targetWidth)
-        const canvas = this.resizeImage(image, targetWidth, targetHeight)
+        const dimensions = this.getProportionalDimensions(image, targetWidth, targetHeight)
+        const canvas = this.resizeImage(image, dimensions.width, dimensions.height)
         return this.canvasToBlob(canvas, 'image/png')
       })
       .then(blob => {
@@ -63,6 +66,35 @@ module.exports = class ThumbnailGenerator extends Plugin {
       })
   }
 
+  /**
+   * Get the new calculated dimensions for the given image and a target width
+   * or height. If both width and height are given, only width is taken into
+   * account. If neither width nor height are given, the default dimension
+   * is used.
+   */
+  getProportionalDimensions (img, width, height) {
+    const aspect = img.width / img.height
+
+    if (width != null) {
+      return {
+        width: width,
+        height: Math.round(width / aspect)
+      }
+    }
+
+    if (height != null) {
+      return {
+        width: Math.round(height * aspect),
+        height: height
+      }
+    }
+
+    return {
+      width: this.defaultThumbnailDimension,
+      height: Math.round(this.defaultThumbnailDimension / aspect)
+    }
+  }
+
   /**
    * Make sure the image doesn’t exceed browser/device canvas limits.
    * For ios with 256 RAM and ie
@@ -148,11 +180,6 @@ module.exports = class ThumbnailGenerator extends Plugin {
     })
   }
 
-  getProportionalHeight (img, width) {
-    const aspect = img.width / img.height
-    return Math.round(width / aspect)
-  }
-
   /**
    * Set the preview URL for a file.
    */
@@ -183,7 +210,7 @@ module.exports = class ThumbnailGenerator extends Plugin {
 
   requestThumbnail (file) {
     if (isPreviewSupported(file.type) && !file.isRemote) {
-      return this.createThumbnail(file, this.opts.thumbnailWidth)
+      return this.createThumbnail(file, this.opts.thumbnailWidth, this.opts.thumbnailHeight)
         .then(preview => {
           this.setPreviewURL(file.id, preview)
           this.uppy.log(`[ThumbnailGenerator] Generated thumbnail for ${file.id}`)
@@ -198,24 +225,45 @@ module.exports = class ThumbnailGenerator extends Plugin {
     return Promise.resolve()
   }
 
+  onFileAdded (file) {
+    if (!file.preview) {
+      this.addToQueue(file)
+    }
+  }
+
+  onFileRemoved (file) {
+    const index = this.queue.indexOf(file)
+    if (index !== -1) {
+      this.queue.splice(index, 1)
+    }
+
+    // Clean up object URLs.
+    if (file.preview && isObjectURL(file.preview)) {
+      URL.revokeObjectURL(file.preview)
+    }
+  }
+
   onRestored () {
-    const fileIDs = Object.keys(this.uppy.getState().files)
+    const { files } = this.uppy.getState()
+    const fileIDs = Object.keys(files)
     fileIDs.forEach((fileID) => {
       const file = this.uppy.getFile(fileID)
       if (!file.isRestored) return
       // Only add blob URLs; they are likely invalid after being restored.
-      if (!file.preview || /^blob:/.test(file.preview)) {
+      if (!file.preview || isObjectURL(file.preview)) {
         this.addToQueue(file)
       }
     })
   }
 
   install () {
-    this.uppy.on('file-added', this.addToQueue)
+    this.uppy.on('file-added', this.onFileAdded)
+    this.uppy.on('file-removed', this.onFileRemoved)
     this.uppy.on('restored', this.onRestored)
   }
   uninstall () {
-    this.uppy.off('file-added', this.addToQueue)
+    this.uppy.off('file-added', this.onFileAdded)
+    this.uppy.off('file-removed', this.onFileRemoved)
     this.uppy.off('restored', this.onRestored)
   }
 }

+ 81 - 19
packages/@uppy/thumbnail-generator/src/index.test.js

@@ -19,12 +19,16 @@ describe('uploader/ThumbnailGeneratorPlugin', () => {
     expect(plugin instanceof Plugin).toEqual(true)
   })
 
-  it('should accept the thumbnailWidth option and override the default', () => {
+  it('should accept the thumbnailWidth and thumbnailHeight option and override the default', () => {
     const plugin1 = new ThumbnailGeneratorPlugin(new MockCore()) // eslint-disable-line no-new
-    expect(plugin1.opts.thumbnailWidth).toEqual(200)
+    expect(plugin1.opts.thumbnailWidth).toEqual(null)
+    expect(plugin1.opts.thumbnailHeight).toEqual(null)
 
     const plugin2 = new ThumbnailGeneratorPlugin(new MockCore(), { thumbnailWidth: 100 }) // eslint-disable-line no-new
     expect(plugin2.opts.thumbnailWidth).toEqual(100)
+
+    const plugin3 = new ThumbnailGeneratorPlugin(new MockCore(), { thumbnailHeight: 100 }) // eslint-disable-line no-new
+    expect(plugin3.opts.thumbnailHeight).toEqual(100)
   })
 
   describe('install', () => {
@@ -37,8 +41,8 @@ describe('uploader/ThumbnailGeneratorPlugin', () => {
       plugin.addToQueue = jest.fn()
       plugin.install()
 
-      expect(core.on).toHaveBeenCalledTimes(2)
-      expect(core.on).toHaveBeenCalledWith('file-added', plugin.addToQueue)
+      expect(core.on).toHaveBeenCalledTimes(3)
+      expect(core.on).toHaveBeenCalledWith('file-added', plugin.onFileAdded)
     })
   })
 
@@ -53,12 +57,12 @@ describe('uploader/ThumbnailGeneratorPlugin', () => {
       plugin.addToQueue = jest.fn()
       plugin.install()
 
-      expect(core.on).toHaveBeenCalledTimes(2)
+      expect(core.on).toHaveBeenCalledTimes(3)
 
       plugin.uninstall()
 
-      expect(core.off).toHaveBeenCalledTimes(2)
-      expect(core.off).toHaveBeenCalledWith('file-added', plugin.addToQueue)
+      expect(core.off).toHaveBeenCalledTimes(3)
+      expect(core.off).toHaveBeenCalledWith('file-added', plugin.onFileAdded)
     })
   })
 
@@ -113,6 +117,44 @@ describe('uploader/ThumbnailGeneratorPlugin', () => {
           expect(plugin.queueProcessing).toEqual(false)
         })
     })
+
+    it('should revoke object URLs when files are removed', async () => {
+      const core = new MockCore()
+      const plugin = new ThumbnailGeneratorPlugin(core)
+      plugin.install()
+
+      URL.revokeObjectURL = jest.fn(() => null)
+
+      try {
+        plugin.createThumbnail = jest.fn(async () => {
+          await delay(50)
+          return 'blob:http://uppy.io/fake-thumbnail'
+        })
+        plugin.setPreviewURL = jest.fn((id, preview) => {
+          if (id === 1) file1.preview = preview
+          if (id === 2) file2.preview = preview
+        })
+
+        const file1 = { id: 1, name: 'bar.jpg', type: 'image/jpeg' }
+        const file2 = { id: 2, name: 'bar2.jpg', type: 'image/jpeg' }
+        core.emit('file-added', file1)
+        core.emit('file-added', file2)
+        expect(plugin.queue).toHaveLength(1)
+        // should drop it from the queue
+        core.emit('file-removed', file2)
+        expect(plugin.queue).toHaveLength(0)
+
+        expect(plugin.createThumbnail).toHaveBeenCalledTimes(1)
+        expect(URL.revokeObjectURL).not.toHaveBeenCalled()
+
+        await delay(110)
+
+        core.emit('file-removed', file1)
+        expect(URL.revokeObjectURL).toHaveBeenCalledTimes(1)
+      } finally {
+        delete URL.revokeObjectURL
+      }
+    })
   })
 
   describe('events', () => {
@@ -168,7 +210,8 @@ describe('uploader/ThumbnailGeneratorPlugin', () => {
         expect(plugin.createThumbnail).toHaveBeenCalledTimes(1)
         expect(plugin.createThumbnail).toHaveBeenCalledWith(
           file,
-          plugin.opts.thumbnailWidth
+          plugin.opts.thumbnailWidth,
+          plugin.opts.thumbnailHeight
         )
       })
     })
@@ -244,19 +287,38 @@ describe('uploader/ThumbnailGeneratorPlugin', () => {
     })
   })
 
-  describe('getProportionalHeight', () => {
-    it('should calculate the resized height based on the specified width of the image whilst keeping aspect ratio', () => {
+  describe('getProportionalDimensions', () => {
+    function resize (thumbnailPlugin, image, width, height) {
+      return thumbnailPlugin.getProportionalDimensions(image, width, height)
+    }
+
+    it('should calculate the thumbnail dimensions based on the width whilst keeping aspect ratio', () => {
+      const core = new MockCore()
+      const plugin = new ThumbnailGeneratorPlugin(core)
+      expect(resize(plugin, { width: 200, height: 100 }, 50)).toEqual({ width: 50, height: 25 })
+      expect(resize(plugin, { width: 66, height: 66 }, 33)).toEqual({ width: 33, height: 33 })
+      expect(resize(plugin, { width: 201.2, height: 198.2 }, 47)).toEqual({ width: 47, height: 46 })
+    })
+
+    it('should calculate the thumbnail dimensions based on the height whilst keeping aspect ratio', () => {
+      const core = new MockCore()
+      const plugin = new ThumbnailGeneratorPlugin(core)
+      expect(resize(plugin, { width: 200, height: 100 }, null, 50)).toEqual({ width: 100, height: 50 })
+      expect(resize(plugin, { width: 66, height: 66 }, null, 33)).toEqual({ width: 33, height: 33 })
+      expect(resize(plugin, { width: 201.2, height: 198.2 }, null, 47)).toEqual({ width: 48, height: 47 })
+    })
+
+    it('should calculate the thumbnail dimensions based on the default width if no custom width is given', () => {
+      const core = new MockCore()
+      const plugin = new ThumbnailGeneratorPlugin(core)
+      plugin.defaultThumbnailDimension = 50
+      expect(resize(plugin, { width: 200, height: 100 })).toEqual({ width: 50, height: 25 })
+    })
+
+    it('should calculate the thumbnail dimensions based on the width if both width and height are given', () => {
       const core = new MockCore()
       const plugin = new ThumbnailGeneratorPlugin(core)
-      expect(
-        plugin.getProportionalHeight({ width: 200, height: 100 }, 50)
-      ).toEqual(25)
-      expect(
-        plugin.getProportionalHeight({ width: 66, height: 66 }, 33)
-      ).toEqual(33)
-      expect(
-        plugin.getProportionalHeight({ width: 201.2, height: 198.2 }, 47)
-      ).toEqual(46)
+      expect(resize(plugin, { width: 200, height: 100 }, 50, 42)).toEqual({ width: 50, height: 25 })
     })
   })
 

+ 1 - 1
packages/@uppy/transloadit/src/AssemblyOptions.js

@@ -19,7 +19,7 @@ function validateParams (params) {
 
   if (!params.auth || !params.auth.key) {
     throw new Error('Transloadit: The `params.auth.key` option is required. ' +
-      'You can find your Transloadit API key at https://transloadit.com/accounts/credentials.')
+      'You can find your Transloadit API key at https://transloadit.com/account/api-settings.')
   }
 }
 

+ 11 - 2
packages/@uppy/transloadit/src/index.js

@@ -15,6 +15,8 @@ function defaultGetAssemblyOptions (file, options) {
 }
 
 const COMPANION = 'https://api2.transloadit.com/companion'
+// Regex matching acceptable postMessage() origins for authentication feedback from companion.
+const ALLOWED_COMPANION_PATTERN = /\.transloadit\.com$/
 // Regex used to check if a Companion address is run by Transloadit.
 const TL_COMPANION = /https?:\/\/api2(?:-\w+)?\.transloadit\.com\/companion/
 const TL_UPPY_SERVER = /https?:\/\/api2(?:-\w+)?\.transloadit\.com\/uppy-server/
@@ -65,8 +67,13 @@ module.exports = class Transloadit extends Plugin {
     this._onRestored = this._onRestored.bind(this)
     this._getPersistentData = this._getPersistentData.bind(this)
 
+    const hasCustomAssemblyOptions = this.opts.getAssemblyOptions !== defaultOptions.getAssemblyOptions
     if (this.opts.params) {
       AssemblyOptions.validateParams(this.opts.params)
+    } else if (!hasCustomAssemblyOptions) {
+      // Throw the same error that we'd throw if the `params` returned from a
+      // `getAssemblyOptions()` function is null.
+      AssemblyOptions.validateParams(null)
     }
 
     this.client = new Client({
@@ -116,10 +123,11 @@ module.exports = class Transloadit extends Plugin {
       this.uppy.log(err)
       throw err
     }
+
     if (file.remote && TL_COMPANION.test(file.remote.serverUrl)) {
-      let newHost = status.companion_url
+      const newHost = status.companion_url
         .replace(/\/$/, '')
-      let path = file.remote.url
+      const path = file.remote.url
         .replace(file.remote.serverUrl, '')
         .replace(/^\//, '')
 
@@ -695,3 +703,4 @@ module.exports = class Transloadit extends Plugin {
 
 module.exports.COMPANION = COMPANION
 module.exports.UPPY_SERVER = COMPANION
+module.exports.COMPANION_PATTERN = ALLOWED_COMPANION_PATTERN

+ 3 - 0
packages/@uppy/utils/src/getFileTypeExtension.js

@@ -7,10 +7,13 @@ const mimeToExtensions = {
   'audio/ogg': 'ogg',
   'video/webm': 'webm',
   'audio/webm': 'webm',
+  'video/x-matroska': 'mkv',
   'video/mp4': 'mp4',
   'audio/mp3': 'mp3'
 }
 
 module.exports = function getFileTypeExtension (mimeType) {
+  // Remove the ; bit in 'video/x-matroska;codecs=avc1'
+  mimeType = mimeType.replace(/;.*$/, '')
   return mimeToExtensions[mimeType] || null
 }

+ 3 - 0
packages/@uppy/utils/src/getFileTypeExtension.test.js

@@ -5,6 +5,9 @@ describe('getFileTypeExtension', () => {
     expect(getFileTypeExtension('video/ogg')).toEqual('ogv')
     expect(getFileTypeExtension('audio/ogg')).toEqual('ogg')
     expect(getFileTypeExtension('video/webm')).toEqual('webm')
+    // Supports mime types with additional data
+    expect(getFileTypeExtension('video/webm;codecs=vp8,opus')).toEqual('webm')
+    expect(getFileTypeExtension('video/x-matroska;codecs=avc1')).toEqual('mkv')
     expect(getFileTypeExtension('audio/webm')).toEqual('webm')
     expect(getFileTypeExtension('video/mp4')).toEqual('mp4')
     expect(getFileTypeExtension('audio/mp3')).toEqual('mp3')

+ 1 - 1
packages/@uppy/utils/src/isPreviewSupported.js

@@ -2,7 +2,7 @@ module.exports = function isPreviewSupported (fileType) {
   if (!fileType) return false
   const fileTypeSpecific = fileType.split('/')[1]
   // list of images that browsers can preview
-  if (/^(jpeg|gif|png|svg|svg\+xml|bmp)$/.test(fileTypeSpecific)) {
+  if (/^(jpe?g|gif|png|svg|svg\+xml|bmp)$/.test(fileTypeSpecific)) {
     return true
   }
   return false

+ 1 - 1
packages/@uppy/utils/src/isPreviewSupported.test.js

@@ -2,7 +2,7 @@ const isPreviewSupported = require('./isPreviewSupported')
 
 describe('isPreviewSupported', () => {
   it('should return true for any filetypes that browsers can preview', () => {
-    const supported = ['image/jpeg', 'image/gif', 'image/png', 'image/svg', 'image/svg+xml', 'image/bmp']
+    const supported = ['image/jpeg', 'image/gif', 'image/png', 'image/svg', 'image/svg+xml', 'image/bmp', 'image/jpg']
     supported.forEach(ext => {
       expect(isPreviewSupported(ext)).toEqual(true)
     })

+ 0 - 7
packages/@uppy/xhr-upload/src/index.js

@@ -288,13 +288,6 @@ module.exports = class XHRUpload extends Plugin {
         }
       })
 
-      this.uppy.on('upload-cancel', (fileID) => {
-        if (fileID === file.id) {
-          timer.done()
-          xhr.abort()
-        }
-      })
-
       this.uppy.on('cancel-all', () => {
         timer.done()
         xhr.abort()

+ 0 - 5
test/endtoend/thumbnails/test.js

@@ -21,11 +21,6 @@ describe('ThumbnailGenerator', () => {
   })
 
   it('should generate thumbnails for images', function () {
-    // FIXME why isn't the selectFakeFile alternative below working?
-    if (!supportsChooseFile()) {
-      return this.skip()
-    }
-
     $('#uppyThumbnails .uppy-FileInput-input').waitForExist()
 
     browser.execute(/* must be valid ES5 for IE */ function () {

+ 23 - 0
test/endtoend/transloadit/index.html

@@ -0,0 +1,23 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <meta charset="utf-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1">
+    <title>Uppy test page</title>
+  </head>
+  <body>
+    <style>
+      main {
+        max-width: 700px;
+        margin: auto;
+      }
+    </style>
+    <main>
+      <h2>Uppy Transloadit</h2>
+      <div id="uppy-transloadit"></div>
+    </main>
+
+    <link href="uppy.min.css" rel="stylesheet">
+    <script src="bundle.js"></script>
+  </body>
+</html>

+ 51 - 0
test/endtoend/transloadit/main.js

@@ -0,0 +1,51 @@
+require('es6-promise/auto')
+require('whatwg-fetch')
+const Uppy = require('@uppy/core')
+const Dashboard = require('@uppy/dashboard')
+const Transloadit = require('@uppy/transloadit')
+
+function initUppyTransloadit (transloaditKey) {
+  var uppyTransloadit = Uppy({
+    id: 'uppyTransloadit',
+    debug: true,
+    autoProceed: true
+  })
+
+  uppyTransloadit
+    .use(Dashboard, {
+      target: '#uppy-transloadit',
+      inline: true
+    })
+    .use(Transloadit, {
+      params: {
+        auth: { key: transloaditKey },
+        steps: {
+          'crop_thumbed': {
+            use: [':original'],
+            robot: '/image/resize',
+            height: 100,
+            resize_strategy: 'crop',
+            width: 100
+          }
+        }
+      },
+      waitForEncoding: true
+    })
+
+  uppyTransloadit.on('transloadit:result', (stepName, result) => {
+    // use transloadit encoding result here.
+    console.log('Result here ====>', stepName, result)
+    console.log('Cropped image url is here ====>', result.url)
+
+    var img = new Image()
+    img.onload = function () {
+      var result = document.createElement('div')
+      result.setAttribute('id', 'uppy-result')
+      result.textContent = 'ok'
+      document.body.appendChild(result)
+    }
+    img.src = result.url
+  })
+}
+
+window.initUppyTransloadit = initUppyTransloadit

+ 55 - 0
test/endtoend/transloadit/test.js

@@ -0,0 +1,55 @@
+/* global browser, expect, capabilities, $ */
+const path = require('path')
+const fs = require('fs')
+const { selectFakeFile, supportsChooseFile } = require('../utils')
+
+const testURL = 'http://localhost:4567/transloadit'
+
+function unhideTheInput () {
+  var input = document.querySelector('#uppy-transloadit .uppy-Dashboard-input')
+  input.removeAttribute('hidden')
+  input.removeAttribute('aria-hidden')
+  input.removeAttribute('tabindex')
+}
+
+function setTransloaditKeyAndInit (transloaditKey) {
+  window.initUppyTransloadit(transloaditKey)
+}
+
+describe('Transloadit file processing', () => {
+  beforeEach(() => {
+    browser.url(testURL)
+  })
+
+  it('should upload a file to Transloadit and crop it', function () {
+    const transloaditKey = process.env.TRANSLOADIT_KEY
+    if (transloaditKey === undefined) {
+      console.log('skipping Transloadit integration test')
+      return this.skip()
+    }
+    browser.execute(setTransloaditKeyAndInit, transloaditKey)
+
+    const inputPath = '#uppy-transloadit .uppy-Dashboard-input'
+    const resultPath = '#uppy-result'
+
+    $(inputPath).waitForExist()
+
+    if (supportsChooseFile(capabilities)) {
+      browser.execute(unhideTheInput)
+      browser.chooseFile(inputPath, path.join(__dirname, '../../resources/image.jpg'))
+    } else {
+      const img = path.join(__dirname, '../../resources/image.jpg')
+      browser.execute(
+        selectFakeFile,
+        'uppyTransloadit',
+        path.basename(img), // name
+        `image/jpeg`, // type
+        fs.readFileSync(img, 'base64') // b64
+      )
+      browser.execute(selectFakeFile, 'uppyTransloadit')
+    }
+    $(resultPath).waitForExist(15000)
+    const text = browser.getText(resultPath)
+    expect(text).to.be.equal('ok')
+  })
+})

+ 22 - 0
test/endtoend/url-plugin/disabled.test.js

@@ -0,0 +1,22 @@
+/* global browser, expect  */
+const testURL = 'http://localhost:4567/url-plugin'
+
+describe('File upload with URL plugin', () => {
+  beforeEach(() => {
+    browser.url(testURL)
+  })
+
+  it('should import  and upload a file completely with Url Plugin', () => {
+    // select url plugin
+    browser.click(`.uppy-DashboardTab-btn[aria-controls=uppy-DashboardContent-panel--Url]`)
+    // import set url value
+    browser.waitForVisible('input.uppy-Url-input', 3000)
+    browser.setValue('input.uppy-Url-input', 'https://github.com/transloadit/uppy/raw/master/assets/palette.png')
+    browser.click('button.uppy-Url-importButton')
+
+    // do the upload
+    browser.waitForVisible('.uppy-u-reset.uppy-c-btn.uppy-c-btn-primary.uppy-StatusBar-actionBtn--upload')
+    browser.click('.uppy-u-reset.uppy-c-btn.uppy-c-btn-primary.uppy-StatusBar-actionBtn--upload')
+    browser.waitForExist('.uppy-StatusBar.is-complete', 20000)
+  })
+})

+ 28 - 0
test/endtoend/url-plugin/index.html

@@ -0,0 +1,28 @@
+<!DOCTYPE html>
+<html>
+
+<head>
+  <meta charset="utf-8">
+  <meta name="viewport" content="width=device-width, initial-scale=1">
+  <title>Uppy test page</title>
+</head>
+
+<body>
+  <style>
+    main {
+      max-width: 700px;
+      margin: auto;
+    }
+  </style>
+  <main>
+    <h2>Uppy URL Plugin</h2>
+    <div>
+      <div id="uppyDashboard"></div>
+    </div>
+  </main>
+
+  <link href="uppy.min.css" rel="stylesheet">
+  <script src="bundle.js"></script>
+</body>
+
+</html>

+ 17 - 0
test/endtoend/url-plugin/main.js

@@ -0,0 +1,17 @@
+require('es6-promise/auto')
+require('whatwg-fetch')
+const Uppy = require('@uppy/core')
+const Dashboard = require('@uppy/dashboard')
+const Url = require('@uppy/url')
+const Tus = require('@uppy/tus')
+
+Uppy({
+  id: 'uppyProvider',
+  debug: true
+})
+  .use(Dashboard, {
+    target: '#uppyDashboard',
+    inline: true
+  })
+  .use(Url, { target: Dashboard, serverUrl: 'http://localhost:3020' })
+  .use(Tus, { endpoint: 'https://master.tus.io/files/' })

+ 25 - 4
test/endtoend/utils.js

@@ -6,11 +6,32 @@ const { spawn } = require('child_process')
 // and IE10/IE11 do not support new syntax features
 function selectFakeFile (uppyID, name, type, b64) {
   if (!b64) b64 = 'PHN2ZyB2aWV3Qm94PSIwIDAgMTIwIDEyMCI+CiAgPGNpcmNsZSBjeD0iNjAiIGN5PSI2MCIgcj0iNTAiLz4KPC9zdmc+Cg=='
+  if (!type) type = 'image/svg+xml'
+
+  // https://stackoverflow.com/questions/16245767/creating-a-blob-from-a-base64-string-in-javascript
+  function base64toBlob (base64Data, contentType) {
+    contentType = contentType || ''
+    var sliceSize = 1024
+    var byteCharacters = atob(base64Data)
+    var bytesLength = byteCharacters.length
+    var slicesCount = Math.ceil(bytesLength / sliceSize)
+    var byteArrays = new Array(slicesCount)
+
+    for (var sliceIndex = 0; sliceIndex < slicesCount; ++sliceIndex) {
+      var begin = sliceIndex * sliceSize
+      var end = Math.min(begin + sliceSize, bytesLength)
+
+      var bytes = new Array(end - begin)
+      for (var offset = begin, i = 0; offset < end; ++i, ++offset) {
+        bytes[i] = byteCharacters[offset].charCodeAt(0)
+      }
+      byteArrays[sliceIndex] = new Uint8Array(bytes)
+    }
+    return new Blob(byteArrays, { type: contentType })
+  }
+
+  var blob = base64toBlob(b64, type)
 
-  var blob = new Blob(
-    ['data:image/svg+xml;base64,' + b64],
-    { type: type || 'image/svg+xml' }
-  )
   window[uppyID].addFile({
     source: 'test',
     name: name || 'test-file',

+ 2 - 0
test/endtoend/wdio.base.conf.js

@@ -93,6 +93,8 @@ exports.config = {
     { mount: '/xhr-limit', path: './test/endtoend/xhr-limit/dist' },
     { mount: '/providers', path: './test/endtoend/providers/dist' },
     { mount: '/thumbnails', path: './test/endtoend/thumbnails/dist' },
+    { mount: '/transloadit', path: './test/endtoend/transloadit/dist' },
+    { mount: '/url-plugin', path: './test/endtoend/url-plugin/dist' },
     { mount: '/create-react-app', path: './test/endtoend/create-react-app/build' }
   ],
 

+ 3 - 3
website/_config.yml

@@ -13,12 +13,12 @@ logo_large: /images/logos/uppy-dog-full.svg
 logo_medium: /images/logos/uppy-dog-head-arrow.svg
 logo_icon: /images/logos/uppy-dog-head-arrow.png
 description: >
-    Sleek, modular file uploader that integrates seamlessly with any framework.
-    It fetches files from local disk, Google Drive, Dropbox, Instagram, remote URLs, cameras and other exciting locations, and then uploads them to the final destination.
+    Sleek, modular file uploader that integrates seamlessly with any website or app.
+    It fetches files from local disk, Google Drive, Dropbox, Instagram, remote URLs, cameras etc, and then uploads them to the final destination.
     It’s fast, easy to use and let's you worry about more important problems than building a file uploader.
 descriptionWho: >
   Uppy is brought to you by the people
-  behind <a href="https://transloadit.com">Transloadit</a> and as such will have first class support
+  behind <a href="https://transloadit.com">Transloadit</a>, and as such will have first class support
   for adding their uploading and encoding backend, but this is opt-in,
   and you can just as easily roll your own.
 author: Transloadit

+ 2 - 2
website/src/_data/design_goals.yml

@@ -1,12 +1,12 @@
  - "Lightweight / easy on dependencies"
+ - "Small core, modular architecture. Everything is a plugin"
  - "ES6, with transpiled ES5 versions available"
  - "Usable as a bundle straight from a CDN, as well as a module to import"
  - "Resumable file uploads via the open tus standard"
  - "Robust: retries for all-the-things, avoid showing ‘weird errors’"
  - "Themable UI with a beautiful default"
  - "i18n support: Easily switch languages or supply your own copy"
- - "Compatible with React (Native)"
+ - "Compatible with React (Native) (work on React Native in progress)"
  - "Works great on mobile"
- - "Small core, modular architecture. Everything is a plugin"
  - "Works great with Transloadit. Works great without"
  - "Offering sugared shortcuts for novice users (presets)"

+ 9 - 1
website/src/api-usage-example.js

@@ -1,9 +1,17 @@
 import Uppy from '@uppy/core'
 import Dashboard from '@uppy/dashboard'
+import Instagram from '@uppy/instagram'
 import Tus from '@uppy/tus'
 
 Uppy()
-  .use(Dashboard, { trigger: '#select-files' })
+  .use(Dashboard, {
+    trigger: '#select-files',
+    showProgressDetails: true
+  })
+  .use(Instagram, {
+    target: Dashboard,
+    serverUrl: 'https://companion.uppy.io'
+  })
   .use(Tus, { endpoint: 'https://master.tus.io/files/' })
   .on('complete', (result) => {
     console.log('Upload result:', result)

+ 6 - 0
website/src/docs/aws-s3.md

@@ -58,6 +58,12 @@ uppy.use(AwsS3, {
 })
 ```
 
+### `serverHeaders: {}`
+
+> Note: This only applies when using [Companion][companion docs] to sign S3 uploads.
+
+Custom headers that should be sent along to [Companion][companion docs] on every request.
+
 ### `getUploadParameters(file)`
 
 > Note: When using [Companion][companion docs] to sign S3 uploads, do not define this option.

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

@@ -222,6 +222,8 @@ The default English strings are:
 strings: {
   // When `inline: false`, used as the screen reader label for the button that closes the modal.
   closeModal: 'Close Modal',
+  // Used as the screen reader label for the plus (+) button that shows the “Add more files” screen
+  addMoreFiles: 'Add more files',
   // Used as the header for import panels, e.g., "Import from Google Drive".
   importFrom: 'Import from %{name}',
   // When `inline: false`, used as the screen reader label for the dashboard modal.

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

@@ -27,7 +27,7 @@ All other props are passed as options to the plugin.
 ```js
 const Uppy = require('@uppy/core')
 const Tus = require('@uppy/tus')
-const DragDrop = require('@uppy/drag-drop')
+const { DragDrop } = require('@uppy/react')
 
 const uppy = Uppy({
   meta: { type: 'avatar' },

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

@@ -55,9 +55,12 @@ const Transloadit = require('@uppy/transloadit')
 
 uppy.use(Dropbox, {
   serverUrl: Transloadit.COMPANION
+  serverPattern: Transloadit.COMPANION_PATTERN
 })
 ```
 
+When using `Transloadit.COMPANION`, you should also configure [`serverPattern: Transloadit.COMPANION_PATTERN`](#Transloadit-COMPANION-PATTERN).
+
 The value of this constant is `https://api2.transloadit.com/companion`. 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 `/companion`:
 
 ```js
@@ -66,6 +69,24 @@ uppy.use(Dropbox, {
 })
 ```
 
+### `Transloadit.COMPANION_PATTERN`
+
+A RegExp pattern matching Transloadit's hosted companion endpoints. The pattern is used in remote provider `serverPattern` options, to ensure that third party authentication messages cannot be faked by an attacker's page, but can only originate from Transloadit's servers.
+
+Use it whenever you use `serverUrl: Transloadit.COMPANION`, like so:
+
+```js
+const Dropbox = require('@uppy/dropbox')
+const Transloadit = require('@uppy/transloadit')
+
+uppy.use(Dropbox, {
+  serverUrl: Transloadit.COMPANION
+  serverPattern: Transloadit.COMPANION_PATTERN
+})
+```
+
+The value of this constant covers _all_ Transloadit's Companion servers, so it does not need to be changed if you are using a custom [`service`](#service) option. However, if you are not using the Transloadit Companion servers at `*.transloadit.com`, make sure to set the `serverPattern` option to something that matches what you do use.
+
 ## Options
 
 The `@uppy/transloadit` plugin has the following configurable options:

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

@@ -36,6 +36,7 @@ The Uppy core module has the following configurable options:
 const uppy = Uppy({
   id: 'uppy',
   autoProceed: false,
+  allowMultipleUploads: true,
   debug: false,
   restrictions: {
     maxFileSize: null,
@@ -69,6 +70,14 @@ const photoUploader = Uppy({ id: 'post' })
 
 By default Uppy will wait for an upload button to be pressed in the UI, or an `.upload()` method to be called, before starting an upload. Setting this to `autoProceed: true` will start uploading automatically after the first file is selected.
 
+### `allowMultipleUploads: true`
+
+Whether to allow multiple upload batches. This means multiple calls to `.upload()`, or a user adding more files after already uploading some. An upload batch is made up of the files that were added since the previous `.upload()` call.
+
+With this option set to `true`, users can upload some files, and then add _more_ files and upload those as well. A model use case for this is uploading images to a gallery or adding attachments to an email.
+
+With this option set to `false`, users can upload some files, and you can listen for the ['complete'](/docs/uppy/#complete) event to continue to the next step in your app's upload flow. A typical use case for this is uploading a new profile picture. If you are integrating with an existing HTML form, this option gives the closest behaviour to a bare `<input type="file">`.
+
 ### `restrictions: {}`
 
 Optionally, provide rules and conditions to limit the type and/or number of files that can be selected.
@@ -239,6 +248,14 @@ const uppy = Uppy()
 uppy.use(DragDrop, { target: 'body' })
 ```
 
+### `uppy.removePlugin(instance)`
+
+Uninstall and remove a plugin.
+
+### `uppy.getPlugin(id)`
+
+Get a plugin by its [`id`](/docs/plugins/#id) to access its methods.
+
 ### `uppy.getID()`
 
 Get the Uppy instance ID, see the [`id` option](#id-39-uppy-39).
@@ -323,6 +340,30 @@ uppy.upload().then((result) => {
 })
 ```
 
+### `uppy.pauseResume(fileID)`
+
+Toggle pause/resume on an upload. Will only work if resumable upload plugin, such as [Tus](/docs/tus/), is used.
+
+### `uppy.pauseAll()`
+
+Pause all uploads. Will only work if a resumable upload plugin, such as [Tus](/docs/tus/), is used.
+
+### `uppy.resumeAll()`
+
+Resume all uploads. Will only work if resumable upload plugin, such as [Tus](/docs/tus/), is used.
+
+### `uppy.retryUpload(fileID)`
+
+Retry an upload (after an error, for example).
+
+### `uppy.retryAll()`
+
+Retry all uploads (after an error, for example).
+
+### `uppy.cancelAll()`
+
+Cancel all uploads, reset progress and remove all files.
+
 ### `uppy.setState(patch)`
 
 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.
@@ -382,6 +423,10 @@ Update the state for a single file. This is mostly useful for plugins that may w
 
 `fileID` is the string file ID. `state` is an object that will be merged into the file's state object.
 
+```js
+uppy.getPlugin('Url').addFile('path/to/remote-file.jpg')
+```
+
 ### `uppy.setMeta(data)`
 
 Alters global `meta` object in state, the one that can be set in Uppy options and gets merged with all newly added files. Calling `setMeta` will also merge newly added meta data with previously selected files.
@@ -543,6 +588,16 @@ uppy.on('upload-error', (file, error) => {
 })
 ```
 
+### `upload-retry`
+
+Fired when an upload has been retried (after an error, for example):
+
+```js
+uppy.on('upload-retry', (fileID) => {
+  console.log('upload retried:', fileID)
+})
+```
+
 ### `info-visible`
 
 Fired when “info” message should be visible in the UI. By default, `Informer` plugin is displaying these messages (enabled by default in `Dashboard` plugin). You can use this event to show messages in your custom UI:
@@ -563,3 +618,7 @@ uppy.on('info-visible', () => {
 ### `info-hidden`
 
 Fired when “info” message should be hidden in the UI. See [`info-visible`](#info-visible).
+
+### `cancel-all`
+
+Fired when [`uppy.cancelAll()`]() is called, all uploads are canceled, files removed and progress is reset.

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

@@ -57,7 +57,11 @@ function initUppy () {
       target: '#uppy-dashboard-container',
       note: 'Images only, 1–2 files, up to 1 MB'
     })
-    .use(Instagram, { target: Dashboard, serverUrl: 'https://api2.transloadit.com/companion', serverPattern: /\.transloadit\.com$/  })
+    .use(Instagram, {
+      target: Dashboard,
+      serverUrl: 'https://api2.transloadit.com/companion',
+      serverPattern: Transloadit.COMPANION_PATTERN
+    })
     .use(Webcam, { target: Dashboard })
 
   uppy

+ 1 - 1
website/src/frontpage-code-sample.ejs

@@ -6,7 +6,7 @@ You can `npm run web:update:frontpage:code:sample` to render this code snippet a
 save it as a layout partial
 -->
 {% codeblock lang:bash %}
-$ npm install uppy
+$ npm install @uppy/core @uppy/dashboard @uppy/instagram @uppy/tus 
 {% endcodeblock %}
 
 {% include_code lang:js ../api-usage-example.js %}

binární
website/src/images/uppy-demo-oct-2018.mp4


+ 22 - 14
website/themes/uppy/layout/index.ejs

@@ -24,8 +24,8 @@
   <div class="IndexExample-block">
     <video class="IndexExample-video"
            autoplay loop muted playsinline>
-      <source src="/images/uppy-demo-2.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.
+      <source src="/images/uppy-demo-oct-2018.mp4" type="video/mp4">
+      Your browser does not support the video tag, you can <a href="/images/uppy-demo-oct-2018.mp4">download the video</a> to watch it.
     </video>
     <form id="upload-form">
       <input type="file">
@@ -42,8 +42,8 @@
 
 <section class="IndexAbout">
   <div class="IndexAbout-item">
-    <img class="IndexAbout-icon" src="/images/traffic-light.svg">
-    <h2 class="IndexSection-title">Work in progress</h2>
+    <!-- <img class="IndexAbout-icon" src="/images/traffic-light.svg"> -->
+    <h2 class="IndexSection-title">What is Uppy</h2>
     <p><%- config.description %></p>
   </div>
   <div class="IndexAbout-item">
@@ -86,18 +86,26 @@
 
 <script>
   var TUS_ENDPOINT = 'https://master.tus.io/files/'
+  var COMPANION_ENDPOINT = 'http://localhost:3020'
+  if (location.hostname === 'uppy.io') {
+    COMPANION_ENDPOINT = '//companion.uppy.io'
+  }
 
   var uppy = Uppy.Core({ debug: true, autoProceed: false })
-  .use(Uppy.Dashboard, {
-    trigger: '#select-files',
-    target: '#upload-form',
-    replaceTargetContent: true,
-    metaFields: [
-      { id: 'license', name: 'License', placeholder: 'specify license' },
-      { id: 'caption', name: 'Caption', placeholder: 'describe what the image is about' }
-    ]
-  })
-  .use(Uppy.Tus, { endpoint: TUS_ENDPOINT})
+    .use(Uppy.Dashboard, {
+      trigger: '#select-files',
+      target: '#upload-form',
+      replaceTargetContent: true,
+      metaFields: [
+        { id: 'license', name: 'License', placeholder: 'specify license' },
+        { id: 'caption', name: 'Caption', placeholder: 'describe what the image is about' }
+      ]
+    })
+    .use(Uppy.GoogleDrive, { target: Uppy.Dashboard, serverUrl: COMPANION_ENDPOINT })
+    .use(Uppy.Instagram, { target: Uppy.Dashboard, serverUrl: COMPANION_ENDPOINT })
+    .use(Uppy.Webcam, { target: Uppy.Dashboard })
+    .use(Uppy.Url, { target: Uppy.Dashboard, serverUrl: COMPANION_ENDPOINT })
+    .use(Uppy.Tus, { endpoint: TUS_ENDPOINT})
 
   uppy.on('success', (files) => {
     console.log(`Upload complete! We’ve uploaded these files: ${files}`)

+ 10 - 2
website/themes/uppy/layout/partials/frontpage-code-sample.html

@@ -2,14 +2,22 @@
 You can `npm run web:update:frontpage:code:sample` to render this code snippet and
 save it as a layout partial
 -->
-<figure class="highlight bash"><table><tr><td class="code"><pre>$ npm install uppy</pre></td></tr></table></figure>
+<figure class="highlight bash"><table><tr><td class="code"><pre>$ <span class="token function">npm</span> <span class="token function">install</span> @uppy/core @uppy/dashboard @uppy/instagram @uppy/tus </pre></td></tr></table></figure>
 
 <figure class="highlight js"><table><tr><td class="code"><pre><span class="token keyword">import</span> Uppy <span class="token keyword">from</span> <span class="token string">'@uppy/core'</span>
 <span class="token keyword">import</span> Dashboard <span class="token keyword">from</span> <span class="token string">'@uppy/dashboard'</span>
+<span class="token keyword">import</span> Instagram <span class="token keyword">from</span> <span class="token string">'@uppy/instagram'</span>
 <span class="token keyword">import</span> Tus <span class="token keyword">from</span> <span class="token string">'@uppy/tus'</span>
 
 <span class="token function">Uppy</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
-  <span class="token punctuation">.</span><span class="token function">use</span><span class="token punctuation">(</span>Dashboard<span class="token punctuation">,</span> <span class="token punctuation">{</span> trigger<span class="token punctuation">:</span> <span class="token string">'#select-files'</span> <span class="token punctuation">}</span><span class="token punctuation">)</span>
+  <span class="token punctuation">.</span><span class="token function">use</span><span class="token punctuation">(</span>Dashboard<span class="token punctuation">,</span> <span class="token punctuation">{</span>
+    trigger<span class="token punctuation">:</span> <span class="token string">'#select-files'</span><span class="token punctuation">,</span>
+    showProgressDetails<span class="token punctuation">:</span> <span class="token boolean">true</span>
+  <span class="token punctuation">}</span><span class="token punctuation">)</span>
+  <span class="token punctuation">.</span><span class="token function">use</span><span class="token punctuation">(</span>Instagram<span class="token punctuation">,</span> <span class="token punctuation">{</span>
+    target<span class="token punctuation">:</span> Dashboard<span class="token punctuation">,</span>
+    serverUrl<span class="token punctuation">:</span> <span class="token string">'https://companion.uppy.io'</span>
+  <span class="token punctuation">}</span><span class="token punctuation">)</span>
   <span class="token punctuation">.</span><span class="token function">use</span><span class="token punctuation">(</span>Tus<span class="token punctuation">,</span> <span class="token punctuation">{</span> endpoint<span class="token punctuation">:</span> <span class="token string">'https://master.tus.io/files/'</span> <span class="token punctuation">}</span><span class="token punctuation">)</span>
   <span class="token punctuation">.</span><span class="token function">on</span><span class="token punctuation">(</span><span class="token string">'complete'</span><span class="token punctuation">,</span> <span class="token punctuation">(</span>result<span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span>
     console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token string">'Upload result:'</span><span class="token punctuation">,</span> result<span class="token punctuation">)</span>

+ 2 - 2
website/themes/uppy/source/css/_index.scss

@@ -118,7 +118,7 @@
 
 .IndexAbout-item:first-child {
   @media #{$screen-medium} {
-    padding-left: 130px;
+    // padding-left: 130px;
 
     .IndexAbout-icon {
       top: 20px;
@@ -285,7 +285,7 @@
 
 .IndexDesignGoals {
   padding: 4em 1em;
-  background-color: $color-primary;
+  background-color: darken($color-primary, 15%);
   color: $color-white;
   overflow: hidden;