Преглед изворни кода

Merge branch 'master' into url-plugin-test-fix

Renée Kooi пре 6 година
родитељ
комит
8cb1f65a2b

+ 4 - 0
CHANGELOG.md

@@ -130,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
 

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

+ 0 - 6
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.
 
@@ -506,11 +505,6 @@ 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) {

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

+ 5 - 0
packages/@uppy/transloadit/src/index.js

@@ -67,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({

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