Bladeren bron

Merge branch 'master' into improvement/better-animation

Artur Paikin 7 jaren geleden
bovenliggende
commit
c6c85c7b22
50 gewijzigde bestanden met toevoegingen van 1059 en 415 verwijderingen
  1. 26 15
      CHANGELOG.md
  2. 3 1
      examples/xhr-bundle/main.js
  3. 8 4
      src/core/Core.js
  4. 4 6
      src/core/Core.test.js
  5. 32 3
      src/core/Translator.js
  6. 16 0
      src/core/Translator.test.js
  7. 6 117
      src/core/Utils.js
  8. 12 2
      src/core/Utils.test.js
  9. 36 0
      src/core/mime-types.js
  10. 9 5
      src/plugins/Dashboard/ActionBrowseTagline.js
  11. 3 3
      src/plugins/Dashboard/Dashboard.js
  12. 13 5
      src/plugins/Dashboard/FileCard.js
  13. 6 5
      src/plugins/Dashboard/FileItem.js
  14. 3 1
      src/plugins/Dashboard/FileItemProgress.js
  15. 29 26
      src/plugins/Dashboard/FileList.js
  16. 2 1
      src/plugins/Dashboard/Tabs.js
  17. 13 5
      src/plugins/Dashboard/index.js
  18. 5 2
      src/plugins/DragDrop/index.js
  19. 3 3
      src/plugins/StatusBar/StatusBar.js
  20. 4 0
      src/plugins/StatusBar/index.js
  21. 14 5
      src/plugins/XHRUpload.js
  22. 1 1
      test/endtoend/src/main.js
  23. 17 0
      website/src/docs/aws-s3.md
  24. 76 47
      website/src/docs/dashboard.md
  25. 28 6
      website/src/docs/dragdrop.md
  26. 53 0
      website/src/docs/dropbox.md
  27. 19 4
      website/src/docs/fileinput.md
  28. 13 1
      website/src/docs/form.md
  29. 53 0
      website/src/docs/google-drive.md
  30. 16 0
      website/src/docs/informer.md
  31. 53 0
      website/src/docs/instagram.md
  32. 6 49
      website/src/docs/plugins.md
  33. 16 0
      website/src/docs/progressbar.md
  34. 37 0
      website/src/docs/providers.md
  35. 79 0
      website/src/docs/react-dashboard-modal.md
  36. 16 51
      website/src/docs/react-dashboard.md
  37. 26 0
      website/src/docs/react-dragdrop.md
  38. 25 0
      website/src/docs/react-progressbar.md
  39. 26 0
      website/src/docs/react-statusbar.md
  40. 11 7
      website/src/docs/react.md
  41. 1 1
      website/src/docs/redux.md
  42. 71 30
      website/src/docs/statusbar.md
  43. 24 0
      website/src/docs/transloadit.md
  44. 6 0
      website/src/docs/tus.md
  45. 16 0
      website/src/docs/uppy.md
  46. 62 0
      website/src/docs/url.md
  47. 32 6
      website/src/docs/webcam.md
  48. 1 1
      website/src/docs/writing-plugins.md
  49. 21 2
      website/src/docs/xhrupload.md
  50. 7 0
      website/themes/uppy/layout/partials/sidebar.ejs

+ 26 - 15
CHANGELOG.md

@@ -48,8 +48,7 @@ PRs are welcome! Please do open an issue to discuss first if it's a big feature,
 - [ ] Webcam modes #198
 - [ ] feature: React Native support
 - [ ] consider iframe / more security for Transloadit/Uppy integration widget and Uppy itself. Page can’t get files from Google Drive if its an iframe; possibility for folder restriction for provider plugins
-- [ ] It would be nice in the long run to have a dynamic package builder here right on the website where you can select the plugins you need/want and it builds and downloads a minified version of them?
-Sort of like jQuery UI: https://jqueryui.com/download/
+- [ ] It would be nice in the long run to have a dynamic package builder here right on the website where you can select the plugins you need/want and it builds and downloads a minified version of them? Sort of like jQuery UI: https://jqueryui.com/download/
 - [ ] test: add https://github.com/pa11y/pa11y for automated accessibility testing?
 - [ ] test: add tests for `npm pack`
 - [ ] test: add deepFreeze to test that state in not mutated anywhere by accident #320
@@ -73,22 +72,25 @@ Sort of like jQuery UI: https://jqueryui.com/download/
 
 What we need to do to release Uppy 1.0
 
-- [ ] QA: test how everything works together: user experience from `npm install` to production build with Webpack, using in React/Redux environment (npm pack)
-- [ ] QA: test in multiple browsers and mobile devices again
-- [x] QA: test uppy server. benchmarks / stress test. multiple connections, different setups, large files (10 GB)
-- [ ] QA: tests for some plugins
+- [ ] website: big release blog post
+- [ ] ~refactoring: Make `uppy-server` module live in main Uppy repo in `./server` as a second stage todo (after Lerna is done and we're happy) (@ife)
+- [ ] QA: manually test in multiple browsers and mobile devices again (SauceLabs can do Android/iOS too) (@nqst)
+- [ ] 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] docs: on using plugins, all options, list of plugins, i18n
-- [ ] feature: preset for Transloadit that mimics jQuery SDK, check https://github.com/transloadit/jquery-sdk docs
-- [ ] refactoring: possibly add CSS-in-JS, style encapsulation
-- [x] refactoring: possibly switch from Yo-Yo to Preact, because it’s more stable, solves a few issues we are struggling with (onload being weird/hard/modern-browsers-only with bel; no way to pass refs to elements; extra network requests with base64 urls) and mature, “new standard”, larger community
-- [ ] refactoring: split uppy into small packages, lerna repo?
-- [x] QA: tests for core and utils
-- [ ] feature: basic Reacte Native support
-- [x] feature: Redux and ReduxDevTools support (currently mirrors Uppy state to Redux)
 - [x] feature: beta file recovering after closed tab / browser crash
 - [x] feature: easy integration with React (UppyReact components)
 - [x] feature: finish the direct-to-s3 upload plugin and test it with the flow to then upload to :transloadit: afterwards. This is because this might influence the inner flow of the plugin architecture quite a bit
+- [x] feature: Redux and ReduxDevTools support (currently mirrors Uppy state to Redux)
 - [x] feature: restrictions: by size, number of files, file type
+- [x] QA: test uppy server. benchmarks / stress test. multiple connections, different setups, large files (10 GB)
+- [x] QA: tests for core and utils
+- [x] refactoring: possibly switch from Yo-Yo to Preact, because it’s more stable, solves a few issues we are struggling with (onload being weird/hard/modern-browsers-only with bel; no way to pass refs to elements; extra network requests with base64 urls) and mature, “new standard”, larger community
 - [x] refactoring: webcam plugin
 - [x] uppy-server: add uppy-server to main API service to scale it horizontally. for the standalone server, we could write the script to support multiple clusters. Not sure how required or neccessary this may be for Transloadit's API service.
 - [x] uppy-server: better error handling, general cleanup (remove unused code. etc)
@@ -119,14 +121,23 @@ To Be Released: 2018-05-31.
 - [ ] uppy-server: benchmarks / stress test, large file, uppy-server / tus / S3 (10 GB)
 - [ ] uppy-server: document docker image setup for uppy-server (@ifedapoolarewaju)
 - [ ] xhrupload: emit a final `upload-progress` event in the XHRUpload plugin just before firing `upload-complete` (tus-js-client already handles this internally) (@arturi)
-- [ ] core: add more mime-to-extension mappings from https://github.com/micnic/mime.json/blob/master/index.json (which ones?) # (@arturi, @goto-bus-stop)
+- [x] core: add more mime-to-extension mappings from https://github.com/micnic/mime.json/blob/master/index.json (which ones?) (#806 /@arturi, @goto-bus-stop)
 - [ ] providers: select files only after “select” is pressed, don’t add them right away when they are checked (keep a list of fileIds in state?); better UI + solves issue with autoProceed uploading in background, which is weird; re-read https://github.com/transloadit/uppy/pull/419#issuecomment-345210519 (@arturi, @goto-bus-stop)
 - [x] tus: add `filename` and `filetype`, so that tus servers knows what headers to set  https://github.com/tus/tus-js-client/commit/ebc5189eac35956c9f975ead26de90c896dbe360 (#844 / @vith)
 - [ ] core: look into utilizing https://github.com/que-etc/resize-observer-polyfill for responsive components. See also https://github.com/transloadit/uppy/issues/750
-- [x] core: removed .run() (to solve issues like #756), update ddocs (#793 / goto-bus-stop)
+- [x] core: ⚠️ **breaking** removed .run() (to solve issues like #756), update docs (#793 / goto-bus-stop)
 - [ ] core: use Browserslist config to share between PostCSS, Autoprefixer and Babel https://github.com/browserslist/browserslist, https://github.com/amilajack/eslint-plugin-compat (@arturi)
 - [ ] core: utilize https://github.com/jonathantneal/postcss-preset-env, maybe https://github.com/jonathantneal/postcss-normalize (@arturi)
+- [ ] core: addFile not passing restrictions shouldn’t throw when called from UI
 - [ ] docs: improve on React docs https://uppy.io/docs/react/, add small example for each component maybe? Dashboard, DragDrop, ProgressBar? No need to make separate pages for all of them, just headings on the same page. Right now docs are confusing, because they focus on DashboardModal. Also problems with syntax highlight on https://uppy.io/docs/react/dashboard-modal/ (@goto-bus-stop)
+- [x] docs: individual React component pages, more plugin options, better groups (#845 / @goto-bus-stop)
+- [x] core: ⚠️ **breaking** Changed some of the strings that we were concatenating in Preact, now their interpolation is handled by the Translator instead. This is important for languages that have different word order than English. (#845 / @goto-bus-stop)
+  Changed strings:
+    - 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
 - [ ] core: customizing metadata fields, boolean metadata; see #809, #454 and related (@arturi)
 - [x] providers: Add user/account names to Uppy provider views (61bf0a7 / @ifedapoolarewaju)
 - [x] s3: implement multipart uploads (#726 / @goto-bus-stop)

+ 3 - 1
examples/xhr-bundle/main.js

@@ -9,7 +9,9 @@ const uppy = Uppy({
 
 uppy.use(Dashboard, {
   target: '#app',
-  inline: true
+  inline: true,
+  hideRetryButton: true,
+  hideCancelButton: true
 })
 
 uppy.use(XHRUpload, {

+ 8 - 4
src/core/Core.js

@@ -31,7 +31,7 @@ class Uppy {
         exceedsSize: 'This file exceeds maximum allowed size of',
         youCanOnlyUploadFileTypes: 'You can only upload:',
         uppyServerError: 'Connection with Uppy Server failed',
-        failedToUpload: 'Failed to upload',
+        failedToUpload: 'Failed to upload %{file}',
         noInternetConnection: 'No Internet connection',
         connectedToInternet: 'Connected to the Internet',
         noFilesFound: 'You have no files or folders here'
@@ -182,6 +182,10 @@ class Uppy {
   * Shorthand to set state for a specific file.
   */
   setFileState (fileID, state) {
+    if (!this.getState().files[fileID]) {
+      throw new Error(`Can’t set state for ${fileID} (the file could have been removed)`)
+    }
+
     this.setState({
       files: Object.assign({}, this.getState().files, {
         [fileID]: Object.assign({}, this.getState().files[fileID], state)
@@ -582,7 +586,8 @@ class Uppy {
 
     this.setState({
       files: {},
-      totalProgress: 0
+      totalProgress: 0,
+      error: null
     })
   }
 
@@ -657,8 +662,7 @@ class Uppy {
       this.setFileState(file.id, { error: error.message })
       this.setState({ error: error.message })
 
-      let message
-      message = `${this.i18n('failedToUpload')} ${file.name}`
+      let message = this.i18n('failedToUpload', { file: file.name })
       if (typeof error === 'object' && error.message) {
         message = { message: message, details: error.message }
       }

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

@@ -21,11 +21,6 @@ describe('src/Core', () => {
     jest.spyOn(utils, 'findDOMElement').mockImplementation(path => {
       return 'some config...'
     })
-    jest.spyOn(utils, 'createThumbnail').mockImplementation(path => {
-      return Promise.resolve(`data:image/jpeg;base64,${sampleImage.toString('base64')}`)
-    })
-    utils.createThumbnail.mockClear()
-
     global.URL.createObjectURL = jest.fn().mockReturnValue('newUrl')
   })
 
@@ -237,6 +232,7 @@ describe('src/Core', () => {
       capabilities: { resumableUploads: false },
       files: {},
       currentUploads: {},
+      error: null,
       foo: 'bar',
       info: { isHidden: true, message: '', type: 'info' },
       meta: {},
@@ -276,6 +272,7 @@ describe('src/Core', () => {
       capabilities: { resumableUploads: false },
       files: {},
       currentUploads: {},
+      error: null,
       info: { isHidden: true, message: '', type: 'info' },
       meta: {},
       plugins: {},
@@ -1111,9 +1108,10 @@ describe('src/Core', () => {
     it('should update the state when receiving the upload-error event', () => {
       const core = new Core()
       core.state.files['fileId'] = {
+        id: 'fileId',
         name: 'filename'
       }
-      core.emit('upload-error', core.state.files['fileId'], new Error('this is the error'))
+      core.emit('upload-error', core.getState().files['fileId'], new Error('this is the error'))
       expect(core.state.info).toEqual({'message': 'Failed to upload filename', 'details': 'this is the error', 'isHidden': false, 'type': 'error'})
     })
 

+ 32 - 3
src/core/Translator.js

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

+ 16 - 0
src/core/Translator.test.js

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

+ 6 - 117
src/core/Utils.js

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

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

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

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

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

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

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

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

@@ -8,11 +8,11 @@ const { h } = require('preact')
 // http://dev.edenspiekermann.com/2016/02/11/introducing-accessible-modal-dialog
 // https://github.com/ghosh/micromodal
 
-const renderInnerPanel = (props) => {
+const PanelContent = (props) => {
   return <div style={{ width: '100%', height: '100%' }}>
     <div class="uppy-DashboardContent-bar">
       <div class="uppy-DashboardContent-title">
-        {props.i18n('importFrom')} {props.activePanel ? props.activePanel.name : null}
+        {props.i18n('importFrom', { name: props.activePanel.name })}
       </div>
       <button class="uppy-DashboardContent-back"
         type="button"
@@ -75,7 +75,7 @@ module.exports = function Dashboard (props) {
             role="tabpanel"
             id={props.activePanel && `uppy-DashboardContent-panel--${props.activePanel.id}`}
             aria-hidden={props.activePanel ? 'false' : 'true'}>
-            {props.activePanel && renderInnerPanel(props)}
+            {props.activePanel && <PanelContent {...props} />}
           </div>
 
           <div class="uppy-Dashboard-progressindicators">

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

@@ -56,13 +56,21 @@ module.exports = class FileCard extends Component {
   }
 
   render () {
+    if (!this.props.fileCardFor) {
+      return <div class="uppy-DashboardFileCard" aria-hidden />
+    }
+
     const file = this.props.files[this.props.fileCardFor]
 
-    return <div class="uppy-DashboardFileCard" aria-hidden={!this.props.fileCardFor}>
-      {this.props.fileCardFor &&
+    return (
+      <div class="uppy-DashboardFileCard" aria-hidden={!this.props.fileCardFor}>
         <div style={{ width: '100%', height: '100%' }}>
           <div class="uppy-DashboardContent-bar">
-            <h2 class="uppy-DashboardContent-title">{this.props.i18n('editing')} <span class="uppy-DashboardContent-titleFile">{file.meta ? file.meta.name : file.name}</span></h2>
+            <h2 class="uppy-DashboardContent-title">
+              {this.props.i18nArray('editing', {
+                file: <span class="uppy-DashboardContent-titleFile">{file.meta ? file.meta.name : file.name}</span>
+              })}
+            </h2>
             <button class="uppy-DashboardContent-back" type="button" title={this.props.i18n('finishEditingFile')}
               onclick={this.handleSave}>{this.props.i18n('done')}</button>
           </div>
@@ -86,7 +94,7 @@ module.exports = class FileCard extends Component {
             </div>
           </div>
         </div>
-      }
-    </div>
+      </div>
+    )
   }
 }

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

@@ -25,13 +25,13 @@ module.exports = function fileItem (props) {
 
   const onPauseResumeCancelRetry = (ev) => {
     if (isUploaded) return
-    if (error) {
+    if (error && !props.hideRetryButton) {
       props.retryUpload(file.id)
       return
     }
     if (props.resumableUploads) {
       props.pauseUpload(file.id)
-    } else {
+    } else if (!props.hideCancelButton) {
       props.cancelUpload(file.id)
     }
   }
@@ -79,10 +79,11 @@ module.exports = function fileItem (props) {
             title={progressIndicatorTitle}
             onclick={onPauseResumeCancelRetry}>
             {error
-              ? iconRetry()
+              ? props.hideCancelButton ? null : iconRetry()
               : FileItemProgress({
                 progress: file.progress.percentage,
-                fileID: file.id
+                fileID: file.id,
+                hideCancelButton: props.hideCancelButton
               })
             }
           </button>
@@ -103,7 +104,7 @@ module.exports = function fileItem (props) {
         {file.source && <div class="uppy-DashboardItem-sourceIcon">
             {acquirers.map(acquirer => {
               if (acquirer.id === file.source) {
-                return <span title={`${props.i18n('fileSource')}: ${acquirer.name}`}>
+                return <span title={props.i18n('fileSource', { name: acquirer.name })}>
                   {acquirer.icon()}
                 </span>
               }

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

@@ -25,7 +25,9 @@ module.exports = (props) => {
         <rect x="5" y="0" width="2" height="10" rx="0" />
       </g>
       <polygon transform="translate(2, 3)" points="14 22.5 7 15.2457065 8.99985857 13.1732815 14 18.3547104 22.9729883 9 25 11.1005634" class="check" />
-      <polygon class="cancel" transform="translate(2, 2)" points="19.8856516 11.0625 16 14.9481516 12.1019737 11.0625 11.0625 12.1143484 14.9481516 16 11.0625 19.8980263 12.1019737 20.9375 16 17.0518484 19.8856516 20.9375 20.9375 19.8980263 17.0518484 16 20.9375 12" />
+      {props.hideCancelButton ? null
+        : <polygon class="cancel" transform="translate(2, 2)" points="19.8856516 11.0625 16 14.9481516 12.1019737 11.0625 11.0625 12.1143484 14.9481516 16 11.0625 19.8980263 12.1019737 20.9375 16 17.0518484 19.8856516 20.9375 20.9375 19.8980263 17.0518484 16 20.9375 12" />
+      }
     </svg>
   )
 }

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

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

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

@@ -23,7 +23,8 @@ class Tabs extends Component {
             <ActionBrowseTagline
               acquirers={this.props.acquirers}
               handleInputChange={this.props.handleInputChange}
-              i18n={this.props.i18n} />
+              i18n={this.props.i18n}
+              i18nArray={this.props.i18nArray} />
           </div>
         </div>
       )

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

@@ -44,25 +44,25 @@ module.exports = class Dashboard extends Plugin {
         selectToUpload: 'Select files to upload',
         closeModal: 'Close Modal',
         upload: 'Upload',
-        importFrom: 'Import from',
+        importFrom: 'Import from %{name}',
         dashboardWindowTitle: 'Uppy Dashboard Window (Press escape to close)',
         dashboardTitle: 'Uppy Dashboard',
         copyLinkToClipboardSuccess: 'Link copied to clipboard',
         copyLinkToClipboardFallback: 'Copy the URL below',
         copyLink: 'Copy link',
-        fileSource: 'File source',
+        fileSource: 'File source: %{name}',
         done: 'Done',
         name: 'Name',
         removeFile: 'Remove file',
         editFile: 'Edit file',
-        editing: 'Editing',
+        editing: 'Editing %{file}',
         finishEditingFile: 'Finish editing file',
         saveChanges: 'Save changes',
         cancel: 'Cancel',
         localDisk: 'Local Disk',
         myDevice: 'My Device',
-        dropPasteImport: 'Drop files here, paste, import from one of the locations above or',
-        dropPaste: 'Drop files here, paste or',
+        dropPasteImport: 'Drop files here, paste, import from one of the locations above or %{browse}',
+        dropPaste: 'Drop files here, paste or %{browse}',
         browse: 'browse',
         fileProgress: 'File progress: upload speed and ETA',
         numberOfSelectedFiles: 'Number of selected files',
@@ -100,6 +100,8 @@ module.exports = class Dashboard extends Plugin {
       showLinkToFileUploadResult: true,
       showProgressDetails: false,
       hideUploadButton: false,
+      hideRetryButton: false,
+      hideCancelButton: false,
       hideProgressAfterFinish: false,
       note: null,
       closeModalOnClickOutside: false,
@@ -121,6 +123,7 @@ module.exports = class Dashboard extends Plugin {
 
     this.translator = new Translator({locale: this.locale})
     this.i18n = this.translator.translate.bind(this.translator)
+    this.i18nArray = this.translator.translateArray.bind(this.translator)
 
     this.openModal = this.openModal.bind(this)
     this.closeModal = this.closeModal.bind(this)
@@ -475,6 +478,8 @@ module.exports = class Dashboard extends Plugin {
       progressindicators: progressindicators,
       autoProceed: this.uppy.opts.autoProceed,
       hideUploadButton: this.opts.hideUploadButton,
+      hideRetryButton: this.opts.hideRetryButton,
+      hideCancelButton: this.opts.hideCancelButton,
       id: this.id,
       closeModal: this.requestCloseModal,
       handleClickOutside: this.handleClickOutside,
@@ -485,6 +490,7 @@ module.exports = class Dashboard extends Plugin {
       hideAllPanels: this.hideAllPanels,
       log: this.uppy.log,
       i18n: this.i18n,
+      i18nArray: this.i18nArray,
       addFile: this.uppy.addFile,
       removeFile: this.uppy.removeFile,
       info: this.uppy.info,
@@ -545,6 +551,8 @@ module.exports = class Dashboard extends Plugin {
         id: `${this.id}:StatusBar`,
         target: this,
         hideUploadButton: this.opts.hideUploadButton,
+        hideRetryButton: this.opts.hideRetryButton,
+        hideCancelButton: this.opts.hideCancelButton,
         showProgressDetails: this.opts.showProgressDetails,
         hideAfterFinish: this.opts.hideProgressAfterFinish,
         locale: this.opts.locale

+ 5 - 2
src/plugins/DragDrop/index.js

@@ -17,7 +17,7 @@ module.exports = class DragDrop extends Plugin {
 
     const defaultLocale = {
       strings: {
-        dropHereOr: 'Drop files here or',
+        dropHereOr: 'Drop files here or %{browse}',
         browse: 'browse'
       }
     }
@@ -44,6 +44,7 @@ module.exports = class DragDrop extends Plugin {
     // i18n
     this.translator = new Translator({locale: this.locale})
     this.i18n = this.translator.translate.bind(this.translator)
+    this.i18nArray = this.translator.translateArray.bind(this.translator)
 
     // Bind `this` to class methods
     this.handleDrop = this.handleDrop.bind(this)
@@ -140,7 +141,9 @@ module.exports = class DragDrop extends Plugin {
               }}
               onchange={this.handleInputChange}
               value="" />
-            {this.i18n('dropHereOr')} <span class="uppy-DragDrop-dragText">{this.i18n('browse')}</span>
+            {this.i18nArray('dropHereOr', {
+              browse: <span class="uppy-DragDrop-dragText">{this.i18n('browse')}</span>
+            })}
           </label>
           <span class="uppy-DragDrop-note">{this.opts.note}</span>
         </div>

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

@@ -99,8 +99,8 @@ module.exports = (props) => {
       {progressBarContent}
       <div class="uppy-StatusBar-actions">
         { props.newFiles && !props.hideUploadButton ? <UploadBtn {...props} uploadState={uploadState} /> : null }
-        { props.error ? <RetryBtn {...props} /> : null }
-        { uploadState !== statusBarStates.STATE_WAITING && uploadState !== statusBarStates.STATE_COMPLETE
+        { props.error && !props.hideRetryButton ? <RetryBtn {...props} /> : null }
+        { !props.hideCancelButton && uploadState !== statusBarStates.STATE_WAITING && uploadState !== statusBarStates.STATE_COMPLETE
           ? <CancelBtn {...props} />
           : null
         }
@@ -161,7 +161,7 @@ const PauseResumeButtons = (props) => {
         : <svg aria-hidden="true" class="UppyIcon" width="16" height="17" viewBox="0 0 12 13">
           <path d="M4.888.81v11.38c0 .446-.324.81-.722.81H2.722C2.324 13 2 12.636 2 12.19V.81c0-.446.324-.81.722-.81h1.444c.398 0 .722.364.722.81zM9.888.81v11.38c0 .446-.324.81-.722.81H7.722C7.324 13 7 12.636 7 12.19V.81c0-.446.324-.81.722-.81h1.444c.398 0 .722.364.722.81z" />
         </svg>
-      : <svg aria-hidden="true" class="UppyIcon" width="16px" height="16px" viewBox="0 0 19 19">
+      : props.hideCancelButton ? null : <svg aria-hidden="true" class="UppyIcon" width="16px" height="16px" viewBox="0 0 19 19">
         <path d="M17.318 17.232L9.94 9.854 9.586 9.5l-.354.354-7.378 7.378h.707l-.62-.62v.706L9.318 9.94l.354-.354-.354-.354L1.94 1.854v.707l.62-.62h-.706l7.378 7.378.354.354.354-.354 7.378-7.378h-.707l.622.62v-.706L9.854 9.232l-.354.354.354.354 7.378 7.378.708-.707-7.38-7.378v.708l7.38-7.38.353-.353-.353-.353-.622-.622-.353-.353-.354.352-7.378 7.38h.708L2.56 1.23 2.208.88l-.353.353-.622.62-.353.355.352.353 7.38 7.38v-.708l-7.38 7.38-.353.353.352.353.622.622.353.353.354-.353 7.38-7.38h-.708l7.38 7.38z" />
       </svg>
     }

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

@@ -54,6 +54,8 @@ module.exports = class StatusBar extends Plugin {
     const defaultOptions = {
       target: 'body',
       hideUploadButton: false,
+      hideRetryButton: false,
+      hideCancelButton: false,
       showProgressDetails: false,
       locale: defaultLocale,
       hideAfterFinish: true
@@ -217,6 +219,8 @@ module.exports = class StatusBar extends Plugin {
       resumableUploads: resumableUploads,
       showProgressDetails: this.opts.showProgressDetails,
       hideUploadButton: this.opts.hideUploadButton,
+      hideRetryButton: this.opts.hideRetryButton,
+      hideCancelButton: this.opts.hideCancelButton,
       hideAfterFinish: this.opts.hideAfterFinish
     })
   }

+ 14 - 5
src/plugins/XHRUpload.js

@@ -126,6 +126,8 @@ module.exports = class XHRUpload extends Plugin {
   createProgressTimeout (timeout, timeoutHandler) {
     const uppy = this.uppy
     const self = this
+    let isDone = false
+
     function onTimedOut () {
       uppy.log(`[XHRUpload] timed out`)
       const error = new Error(self.i18n('timedOut', { seconds: Math.ceil(timeout / 1000) }))
@@ -134,17 +136,24 @@ module.exports = class XHRUpload extends Plugin {
 
     let aliveTimer = null
     function progress () {
+      // Some browsers fire another progress event when the upload is
+      // cancelled, so we have to ignore progress after the timer was
+      // told to stop.
+      if (isDone) return
+
       if (timeout > 0) {
-        done()
+        if (aliveTimer) clearTimeout(aliveTimer)
         aliveTimer = setTimeout(onTimedOut, timeout)
       }
     }
 
     function done () {
+      uppy.log(`[XHRUpload] timer done`)
       if (aliveTimer) {
         clearTimeout(aliveTimer)
         aliveTimer = null
       }
+      isDone = true
     }
 
     return {
@@ -281,8 +290,7 @@ module.exports = class XHRUpload extends Plugin {
       })
 
       this.uppy.on('cancel-all', () => {
-        // const files = this.uppy.getState().files
-        // if (!files[file.id]) return
+        timer.done()
         xhr.abort()
       })
     })
@@ -387,8 +395,8 @@ module.exports = class XHRUpload extends Plugin {
         files.forEach((file) => {
           this.uppy.emit('upload-progress', file, {
             uploader: this,
-            bytesUploaded: ev.loaded,
-            bytesTotal: ev.total
+            bytesUploaded: ev.loaded / ev.total * file.size,
+            bytesTotal: file.size
           })
         })
       })
@@ -419,6 +427,7 @@ module.exports = class XHRUpload extends Plugin {
       })
 
       this.uppy.on('cancel-all', () => {
+        timer.done()
         xhr.abort()
       })
 

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

@@ -24,7 +24,7 @@ const uppyi18n = Uppy({
     target: '#uppyi18n',
     locale: {
       strings: {
-        dropHereOr: 'Перенесите файлы сюда или',
+        dropHereOr: 'Перенесите файлы сюда или %{browse}',
         browse: 'выберите'
       }
     }

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

@@ -25,6 +25,10 @@ There is also a separate plugin for S3 Multipart uploads. Multipart in this sens
 
 ## Options
 
+### `id: 'AwsS3'`
+
+A unique identifier for this plugin. Defaults to `'AwsS3'`.
+
 ### `host`
 
 When using [uppy-server][uppy-server docs] to sign S3 uploads, set this option to the root URL of the uppy-server.
@@ -66,6 +70,19 @@ The default is 30 seconds.
 Limit the amount of uploads going on at the same time. This is passed through to [XHRUpload](/docs/xhrupload#limit-0); see its documentation page for details.
 Set to `0` to disable limiting.
 
+### `locale: {}`
+
+Localize text that is shown to the user.
+
+The default English strings are:
+
+```js
+strings: {
+  // Shown in the StatusBar while the upload is being signed.
+  preparingUpload: 'Preparing upload...'
+}
+```
+
 ## S3 Bucket configuration
 
 S3 buckets do not allow public uploads by default.

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

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

+ 28 - 6
website/src/docs/dragdrop.md

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

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

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

+ 19 - 4
website/src/docs/fileinput.md

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@@ -2,7 +2,7 @@
 title: "Redux"
 type: docs
 permalink: docs/redux
-order: 57
+order: 67
 ---
 
 Uppy supports popular [Redux](https://redux.js.org/) state management library in two ways:

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

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

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

@@ -10,6 +10,8 @@ The Transloadit plugin can be used to upload files to [Transloadit](https://tran
 [Try it live](/examples/transloadit/)
 
 ```js
+const Transloadit = require('uppy/lib/plugins/Transloadit')
+
 uppy.use(Transloadit, {
   service: 'https://api2.transloadit.com',
   params: null,
@@ -49,6 +51,10 @@ uppy.use(Dropbox, {
 
 ## Options
 
+### `id: 'Transloadit'`
+
+A unique identifier for this plugin. Defaults to `'Transloadit'`.
+
 ### `service`
 
 The Transloadit API URL to use. Defaults to `https://api2.transloadit.com`, which will attempt to route traffic efficiently based on where your users are. You can set this to something like `https://api2-us-east-1.transloadit.com` if you want to use a particular region.
@@ -205,6 +211,24 @@ uppy.use(Transloadit, {
 })
 ```
 
+### `locale: {}`
+
+Localize text that is shown to the user.
+
+The default English strings are:
+
+```js
+strings: {
+  // Shown while Assemblies are being created for an upload.
+  creatingAssembly: 'Preparing upload...'
+  // Shown if an Assembly could not be created.
+  creatingAssemblyFailed: 'Transloadit: Could not create Assembly',
+  // Shown after uploads have succeeded, but when the Assembly is still executing.
+  // This only shows if `waitForMetadata` or `waitForEncoding` was set.
+  encoding: 'Encoding...'
+}
+```
+
 ## Events
 
 ### `transloadit:assembly-created`

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

@@ -8,6 +8,8 @@ permalink: docs/tus/
 The Tus plugin brings [tus.io](http://tus.io) resumable file uploading to Uppy by wrapping the [tus-js-client][].
 
 ```js
+const Tus = require('uppy/lib/plugins/Tus')
+
 uppy.use(Tus, {
   endpoint: 'https://master.tus.io/files/', // use your tus endpoint here
   resume: true,
@@ -20,6 +22,10 @@ uppy.use(Tus, {
 
 The Tus plugin supports all of [tus-js-client][]’s options. Additionally:
 
+### `id: 'Tus'`
+
+A unique identifier for this plugin. Defaults to `'Tus'`.
+
 ### `resume: true`
 
 A boolean indicating whether tus should attempt to resume the upload if the upload has been started in the past. This includes storing the file’s upload url. Use false to force an entire reupload.

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

@@ -7,6 +7,12 @@ permalink: docs/uppy/
 
 Core module that orchestrates everything in Uppy, exposing `state`, `events` and `methods`.
 
+```js
+const Uppy = require('uppy/lib/core')
+
+const uppy = Uppy()
+```
+
 ## Options
 
 ```js
@@ -238,6 +244,16 @@ uppy.addFile({
 
 If `uppy.opts.autoProceed === true`, Uppy will begin uploading automatically when files are added.
 
+### `uppy.removeFile(fileID)`
+
+Remove a file from Uppy.
+
+```js
+uppy.removeFile('uppyteamkongjpg1501851828779')
+```
+
+Removing a file that is already being uploaded cancels that upload.
+
 ### `uppy.getFile(fileID)`
 
 Get a specific file object by its ID.

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

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

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

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

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

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

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

@@ -7,16 +7,22 @@ permalink: docs/xhrupload/
 
 The XHRUpload plugin handles classic HTML multipart form uploads, as well as uploads using the HTTP `PUT` method.
 
-[Try it live](/examples/xhrupload/)
-
 ```js
+const XHRUpload = require('uppy/lib/plugins/XHRUpload')
+
 uppy.use(XHRUpload, {
   endpoint: 'http://my-website.org/upload'
 })
 ```
 
+[Try it live](/examples/xhrupload/)
+
 ## Options
 
+### `id: 'XHRUpload'`
+
+A unique identifier for this plugin. Defaults to `'XHRUpload'`.
+
 ### `endpoint: ''`
 
 URL to upload to.
@@ -147,6 +153,19 @@ The default is 30 seconds.
 
 Limit the amount of uploads going on at the same time. Passing `0` means no limit.
 
+### `locale: {}`
+
+Localize text that is shown to the user.
+
+The default English strings are:
+
+```js
+strings: {
+  // Shown in the Informer if an upload is being canceled because it stalled for too long.
+  timedOut: 'Upload stalled for %{seconds} seconds, aborting.'
+}
+```
+
 ## POST Parameters / Form Fields
 
 When using XHRUpload with `formData: true`, file metadata is sent along with each upload request. You can set metadata for a file using [`uppy.setFileMeta(fileID, data)`](/docs/uppy#uppy-setFileMeta-fileID-data), or for all files simultaneously using [`uppy.setMeta(data)`](/docs/uppy#uppy-setMeta-data).

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

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