Ver Fonte

Merge pull request #1431 from transloadit/feature/tl-cancel

transloadit: Assembly cancellation
Artur Paikin há 6 anos atrás
pai
commit
6881eef6f3

+ 4 - 0
examples/transloadit-textarea/main.js

@@ -109,6 +109,8 @@ class MarkdownTextarea {
         template_id: TRANSLOADIT_EXAMPLE_TEMPLATE
       }
     }).then((result) => {
+      // Was cancelled
+      if (result == null) return
       this.insertAttachments(
         this.matchFilesAndThumbs(result.results)
       )
@@ -126,6 +128,8 @@ class MarkdownTextarea {
         template_id: TRANSLOADIT_EXAMPLE_TEMPLATE
       }
     }).then((result) => {
+      // Was cancelled
+      if (result == null) return
       this.insertAttachments(
         this.matchFilesAndThumbs(result.results)
       )

+ 6 - 2
packages/@uppy/core/src/index.js

@@ -123,6 +123,7 @@ class Uppy {
       allowNewUpload: true,
       capabilities: {
         uploadProgress: supportsUploadProgress(),
+        individualCancellation: true,
         resumableUploads: false
       },
       totalProgress: 0,
@@ -1188,7 +1189,6 @@ class Uppy {
       const { currentUploads } = this.getState()
       const currentUpload = currentUploads[uploadID]
       if (!currentUpload) {
-        this.log(`Not setting result for an upload that has been removed: ${uploadID}`)
         return
       }
 
@@ -1204,7 +1204,6 @@ class Uppy {
       // to an outdated object without the `.result` property.
       const { currentUploads } = this.getState()
       if (!currentUploads[uploadID]) {
-        this.log(`Not setting result for an upload that has been canceled: ${uploadID}`)
         return
       }
       const currentUpload = currentUploads[uploadID]
@@ -1213,6 +1212,11 @@ class Uppy {
 
       this._removeUpload(uploadID)
 
+      return result
+    }).then((result) => {
+      if (result == null) {
+        this.log(`Not setting result for an upload that has been removed: ${uploadID}`)
+      }
       return result
     })
   }

+ 6 - 17
packages/@uppy/core/src/index.test.js

@@ -150,7 +150,7 @@ describe('src/Core', () => {
 
       const newState = {
         bee: 'boo',
-        capabilities: { uploadProgress: true, resumableUploads: false },
+        capabilities: { individualCancellation: true, uploadProgress: true, resumableUploads: false },
         files: {},
         currentUploads: {},
         allowNewUpload: true,
@@ -174,7 +174,7 @@ describe('src/Core', () => {
       // current state
       expect(stateUpdateEventMock.mock.calls[1][0]).toEqual({
         bee: 'boo',
-        capabilities: { uploadProgress: true, resumableUploads: false },
+        capabilities: { individualCancellation: true, uploadProgress: true, resumableUploads: false },
         files: {},
         currentUploads: {},
         allowNewUpload: true,
@@ -187,7 +187,7 @@ describe('src/Core', () => {
       // new state
       expect(stateUpdateEventMock.mock.calls[1][1]).toEqual({
         bee: 'boo',
-        capabilities: { uploadProgress: true, resumableUploads: false },
+        capabilities: { individualCancellation: true, uploadProgress: true, resumableUploads: false },
         files: {},
         currentUploads: {},
         allowNewUpload: true,
@@ -204,17 +204,7 @@ describe('src/Core', () => {
 
       core.setState({ foo: 'bar' })
 
-      expect(core.getState()).toEqual({
-        capabilities: { uploadProgress: true, resumableUploads: false },
-        files: {},
-        currentUploads: {},
-        allowNewUpload: true,
-        foo: 'bar',
-        info: { isHidden: true, message: '', type: 'info' },
-        meta: {},
-        plugins: {},
-        totalProgress: 0
-      })
+      expect(core.getState()).toMatchObject({ foo: 'bar' })
     })
   })
 
@@ -229,11 +219,10 @@ describe('src/Core', () => {
 
     core.reset()
 
-    // expect(corePauseEventMock.mock.calls.length).toEqual(1)
     expect(coreCancelEventMock.mock.calls.length).toEqual(1)
     expect(coreStateUpdateEventMock.mock.calls.length).toEqual(2)
     expect(coreStateUpdateEventMock.mock.calls[1][1]).toEqual({
-      capabilities: { uploadProgress: true, resumableUploads: false },
+      capabilities: { individualCancellation: true, uploadProgress: true, resumableUploads: false },
       files: {},
       currentUploads: {},
       allowNewUpload: true,
@@ -292,7 +281,7 @@ describe('src/Core', () => {
     expect(coreCancelEventMock.mock.calls.length).toEqual(1)
     expect(coreStateUpdateEventMock.mock.calls.length).toEqual(1)
     expect(coreStateUpdateEventMock.mock.calls[0][1]).toEqual({
-      capabilities: { uploadProgress: true, resumableUploads: false },
+      capabilities: { individualCancellation: true, uploadProgress: true, resumableUploads: false },
       files: {},
       currentUploads: {},
       allowNewUpload: true,

+ 13 - 7
packages/@uppy/dashboard/src/components/FileItem.js

@@ -15,14 +15,13 @@ function FileItemProgressWrapper (props) {
   }
 
   if (props.isUploaded ||
-      props.bundled ||
       (props.hidePauseResumeCancelButtons && !props.error)) {
     return <div class="uppy-DashboardItem-progressIndicator">
       <FileItemProgress
         progress={props.file.progress.percentage}
         fileID={props.file.id}
         hidePauseResumeCancelButtons={props.hidePauseResumeCancelButtons}
-        bundled={props.bundled}
+        individualCancellation={props.individualCancellation}
       />
     </div>
   }
@@ -38,13 +37,14 @@ function FileItemProgressWrapper (props) {
       : <FileItemProgress
         progress={props.file.progress.percentage}
         fileID={props.file.id}
+        individualCancellation={props.individualCancellation}
         hidePauseResumeCancelButtons={props.hidePauseResumeCancelButtons}
       />
     }
   </button>
 }
 
-module.exports = function fileItem (props) {
+module.exports = function FileItem (props) {
   const file = props.file
   const acquirers = props.acquirers
 
@@ -72,7 +72,7 @@ module.exports = function fileItem (props) {
 
     if (props.resumableUploads) {
       props.pauseUpload(file.id)
-    } else {
+    } else if (props.individualCancellation) {
       props.cancelUpload(file.id)
     }
   }
@@ -91,9 +91,11 @@ module.exports = function fileItem (props) {
         return props.i18n('resumeUpload')
       }
       return props.i18n('pauseUpload')
-    } else {
+    } else if (props.individualCancellation) {
       return props.i18n('cancelUpload')
     }
+
+    return ''
   }
 
   const dashboardItemClass = classNames(
@@ -104,9 +106,13 @@ module.exports = function fileItem (props) {
     { 'is-paused': isPaused },
     { 'is-error': error },
     { 'is-resumable': props.resumableUploads },
-    { 'is-bundled': props.bundledUpload }
+    { 'is-noIndividualCancellation': !props.individualCancellation }
   )
 
+  const showRemoveButton = props.individualCancellation
+    ? !isUploaded
+    : !uploadInProgress && !isUploaded
+
   return <li class={dashboardItemClass} id={`uppy_${file.id}`} title={file.meta.name}>
     <div class="uppy-DashboardItem-preview">
       <div class="uppy-DashboardItem-previewInnerWrap" style={{ backgroundColor: getFileTypeIcon(file.type).color }}>
@@ -174,7 +180,7 @@ module.exports = function fileItem (props) {
       </div>
     </div>
     <div class="uppy-DashboardItem-action">
-      {!isUploaded &&
+      {showRemoveButton &&
         <button class="uppy-DashboardItem-remove"
           type="button"
           aria-label={props.i18n('removeFile')}

+ 1 - 1
packages/@uppy/dashboard/src/components/FileItemProgress.js

@@ -19,7 +19,7 @@ module.exports = (props) => {
           stroke-dashoffset={circleLength - (circleLength / 100 * props.progress)}
         />
       </g>
-      {!props.hidePauseResumeCancelButtons && !props.bundled ? (
+      {!props.hidePauseResumeCancelButtons ? (
         <g>
           <polygon class="play" transform="translate(3, 3)" points="12 20 12 10 20 15" />
           <g class="pause" transform="translate(14.5, 13)">

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

@@ -729,7 +729,7 @@ module.exports = class Dashboard extends Plugin {
       note: this.opts.note,
       metaFields: pluginState.metaFields,
       resumableUploads: capabilities.resumableUploads || false,
-      bundled: capabilities.bundled || false,
+      individualCancellation: capabilities.individualCancellation,
       startUpload,
       pauseUpload: this.uppy.pauseResume,
       retryUpload: this.uppy.retryUpload,

+ 10 - 0
packages/@uppy/dashboard/src/style.scss

@@ -1061,6 +1061,16 @@ a.uppy-Dashboard-poweredBy {
   }
 }
 
+.uppy-DashboardItem.is-noIndividualCancellation {
+  .uppy-DashboardItem-progressIndicator {
+    cursor: default;
+  }
+
+  .cancel {
+    display: none;
+  }
+}
+
 .uppy-DashboardItem.is-processing .uppy-DashboardItem-progress {
   opacity: 0;
 }

+ 53 - 22
packages/@uppy/transloadit/src/AssemblyOptions.test.js

@@ -1,7 +1,7 @@
 const AssemblyOptions = require('./AssemblyOptions')
 
 describe('Transloadit/AssemblyOptions', () => {
-  it('Validates response from getAssemblyOptions()', () => {
+  it('Validates response from getAssemblyOptions()', async () => {
     const options = new AssemblyOptions([
       { name: 'testfile' }
     ], {
@@ -13,12 +13,12 @@ describe('Transloadit/AssemblyOptions', () => {
       }
     })
 
-    return expect(options.build()).rejects.toThrow(
+    await expect(options.build()).rejects.toThrow(
       /The `params\.auth\.key` option is required/
     )
   })
 
-  it('Uses different assemblies for different params', () => {
+  it('Uses different assemblies for different params', async () => {
     const data = Buffer.alloc(10)
     data.size = data.byteLength
 
@@ -38,16 +38,15 @@ describe('Transloadit/AssemblyOptions', () => {
       })
     })
 
-    return options.build().then((assemblies) => {
-      expect(assemblies).toHaveLength(4)
-      expect(assemblies[0].options.params.steps.fake_step.data).toBe('a.png')
-      expect(assemblies[1].options.params.steps.fake_step.data).toBe('b.png')
-      expect(assemblies[2].options.params.steps.fake_step.data).toBe('c.png')
-      expect(assemblies[3].options.params.steps.fake_step.data).toBe('d.png')
-    })
+    const assemblies = await options.build()
+    expect(assemblies).toHaveLength(4)
+    expect(assemblies[0].options.params.steps.fake_step.data).toBe('a.png')
+    expect(assemblies[1].options.params.steps.fake_step.data).toBe('b.png')
+    expect(assemblies[2].options.params.steps.fake_step.data).toBe('c.png')
+    expect(assemblies[3].options.params.steps.fake_step.data).toBe('d.png')
   })
 
-  it('Should merge files with same parameters into one Assembly', () => {
+  it('Should merge files with same parameters into one Assembly', async () => {
     const data = Buffer.alloc(10)
     const data2 = Buffer.alloc(20)
 
@@ -67,26 +66,25 @@ describe('Transloadit/AssemblyOptions', () => {
       })
     })
 
-    return options.build().then((assemblies) => {
-      expect(assemblies).toHaveLength(2)
-      expect(assemblies[0].fileIDs).toHaveLength(3)
-      expect(assemblies[1].fileIDs).toHaveLength(1)
-      expect(assemblies[0].options.params.steps.fake_step.data).toBe(10)
-      expect(assemblies[1].options.params.steps.fake_step.data).toBe(20)
-    })
+    const assemblies = await options.build()
+    expect(assemblies).toHaveLength(2)
+    expect(assemblies[0].fileIDs).toHaveLength(3)
+    expect(assemblies[1].fileIDs).toHaveLength(1)
+    expect(assemblies[0].options.params.steps.fake_step.data).toBe(10)
+    expect(assemblies[1].options.params.steps.fake_step.data).toBe(20)
   })
 
-  it('Does not create an Assembly if no files are being uploaded', () => {
+  it('Does not create an Assembly if no files are being uploaded', async () => {
     const options = new AssemblyOptions([], {
       getAssemblyOptions () {
         throw new Error('should not create Assembly')
       }
     })
 
-    return expect(options.build()).resolves.toEqual([])
+    await expect(options.build()).resolves.toEqual([])
   })
 
-  it('Creates an Assembly if no files are being uploaded but `alwaysRunAssembly` is enabled', () => {
+  it('Creates an Assembly if no files are being uploaded but `alwaysRunAssembly` is enabled', async () => {
     const options = new AssemblyOptions([], {
       alwaysRunAssembly: true,
       getAssemblyOptions (file) {
@@ -100,6 +98,39 @@ describe('Transloadit/AssemblyOptions', () => {
       }
     })
 
-    return expect(options.build()).resolves.toHaveLength(1)
+    await expect(options.build()).resolves.toHaveLength(1)
+  })
+
+  it('Collects metadata if `fields` is an array', async () => {
+    function defaultGetAssemblyOptions (file, options) {
+      return {
+        params: options.params,
+        signature: options.signature,
+        fields: options.fields
+      }
+    }
+
+    const options = new AssemblyOptions([{
+      id: 1,
+      meta: { watermark: 'Some text' }
+    }, {
+      id: 2,
+      meta: { watermark: 'ⓒ Transloadit GmbH' }
+    }], {
+      fields: ['watermark'],
+      params: {
+        auth: { key: 'fake key' }
+      },
+      getAssemblyOptions: defaultGetAssemblyOptions
+    })
+
+    const assemblies = await options.build()
+    expect(assemblies).toHaveLength(2)
+    expect(assemblies[0].options.fields).toMatchObject({
+      watermark: 'Some text'
+    })
+    expect(assemblies[1].options.fields).toMatchObject({
+      watermark: 'ⓒ Transloadit GmbH'
+    })
   })
 })

+ 11 - 0
packages/@uppy/transloadit/src/AssemblyWatcher.js

@@ -22,6 +22,7 @@ class TransloaditAssemblyWatcher extends Emitter {
     })
 
     this._onAssemblyComplete = this._onAssemblyComplete.bind(this)
+    this._onAssemblyCancel = this._onAssemblyCancel.bind(this)
     this._onAssemblyError = this._onAssemblyError.bind(this)
     this._onImportError = this._onImportError.bind(this)
 
@@ -47,6 +48,14 @@ class TransloaditAssemblyWatcher extends Emitter {
     this._checkAllComplete()
   }
 
+  _onAssemblyCancel (assembly) {
+    if (!this._watching(assembly.assembly_id)) {
+      return
+    }
+
+    this._checkAllComplete()
+  }
+
   _onAssemblyError (assembly, error) {
     if (!this._watching(assembly.assembly_id)) {
       return
@@ -84,12 +93,14 @@ class TransloaditAssemblyWatcher extends Emitter {
 
   _removeListeners () {
     this._uppy.off('transloadit:complete', this._onAssemblyComplete)
+    this._uppy.off('transloadit:assembly-cancel', this._onAssemblyCancel)
     this._uppy.off('transloadit:assembly-error', this._onAssemblyError)
     this._uppy.off('transloadit:import-error', this._onImportError)
   }
 
   _addListeners () {
     this._uppy.on('transloadit:complete', this._onAssemblyComplete)
+    this._uppy.on('transloadit:assembly-cancel', this._onAssemblyCancel)
     this._uppy.on('transloadit:assembly-error', this._onAssemblyError)
     this._uppy.on('transloadit:import-error', this._onImportError)
   }

+ 22 - 0
packages/@uppy/transloadit/src/Client.js

@@ -46,12 +46,24 @@ module.exports = class Client {
     })
   }
 
+  /**
+   * Reserve resources for a file in an Assembly. Then addFile can be used later.
+   *
+   * @param {object} assembly
+   * @param {UppyFile} file
+   */
   reserveFile (assembly, file) {
     const size = encodeURIComponent(file.size)
     return fetch(`${assembly.assembly_ssl_url}/reserve_file?size=${size}`, { method: 'post' })
       .then((response) => response.json())
   }
 
+  /**
+   * Import a remote file to an Assembly.
+   *
+   * @param {object} assembly
+   * @param {UppyFile} file
+   */
   addFile (assembly, file) {
     if (!file.uploadURL) {
       return Promise.reject(new Error('File does not have an `uploadURL`.'))
@@ -66,6 +78,16 @@ module.exports = class Client {
       .then((response) => response.json())
   }
 
+  /**
+   * Cancel a running Assembly.
+   *
+   * @param {object} assembly
+   */
+  cancelAssembly (assembly) {
+    return fetch(assembly.assembly_ssl_url, { method: 'delete' })
+      .then((response) => response.json())
+  }
+
   /**
    * Get the current status for an assembly.
    *

+ 51 - 7
packages/@uppy/transloadit/src/index.js

@@ -64,7 +64,8 @@ module.exports = class Transloadit extends Plugin {
 
     this._prepareUpload = this._prepareUpload.bind(this)
     this._afterUpload = this._afterUpload.bind(this)
-    this._handleError = this._handleError.bind(this)
+    this._onError = this._onError.bind(this)
+    this._onCancelAll = this._onCancelAll.bind(this)
     this._onFileUploadURLAvailable = this._onFileUploadURLAvailable.bind(this)
     this._onRestored = this._onRestored.bind(this)
     this._getPersistentData = this._getPersistentData.bind(this)
@@ -329,6 +330,29 @@ module.exports = class Transloadit extends Plugin {
     })
   }
 
+  _cancelAssembly (assembly) {
+    return this.client.cancelAssembly(assembly).then(() => {
+      // TODO bubble this through AssemblyWatcher so its event handlers can clean up correctly
+      this.uppy.emit('transloadit:assembly-cancelled', assembly)
+    })
+  }
+
+  /**
+   * When all files are removed, cancel in-progress Assemblies.
+   */
+  _onCancelAll () {
+    const { assemblies } = this.getPluginState()
+
+    const cancelPromises = Object.keys(assemblies).map((assemblyID) => {
+      const assembly = this.getAssembly(assemblyID)
+      return this._cancelAssembly(assembly)
+    })
+
+    Promise.all(cancelPromises).catch((err) => {
+      this.uppy.log(err)
+    })
+  }
+
   /**
    * Custom state serialization for the Golden Retriever plugin.
    * It will pass this back to the `_onRestored` function.
@@ -632,8 +656,8 @@ module.exports = class Transloadit extends Plugin {
     })
   }
 
-  _handleError (err, uploadID) {
-    this.uppy.log(`[Transloadit] _handleError in upload ${uploadID}`)
+  _onError (err, uploadID) {
+    this.uppy.log(`[Transloadit] _onError in upload ${uploadID}`)
     this.uppy.log(err)
     const state = this.getPluginState()
     const assemblyIDs = state.uploadsAssemblies[uploadID]
@@ -650,7 +674,10 @@ module.exports = class Transloadit extends Plugin {
     this.uppy.addPostProcessor(this._afterUpload)
 
     // We may need to close socket.io connections on error.
-    this.uppy.on('error', this._handleError)
+    this.uppy.on('error', this._onError)
+
+    // Handle cancellation.
+    this.uppy.on('cancel-all', this._onCancelAll)
 
     if (this.opts.importFromUploadURLs) {
       // No uploader needed when importing; instead we take the upload URL from an existing uploader.
@@ -681,21 +708,38 @@ module.exports = class Transloadit extends Plugin {
       // Contains result data from Transloadit.
       results: []
     })
+
+    // We cannot cancel individual files because Assemblies tend to contain many files.
+    const { capabilities } = this.uppy.getState()
+    this.uppy.setState({
+      capabilities: {
+        ...capabilities,
+        individualCancellation: false
+      }
+    })
   }
 
   uninstall () {
     this.uppy.removePreProcessor(this._prepareUpload)
     this.uppy.removePostProcessor(this._afterUpload)
-    this.uppy.off('error', this._handleError)
+    this.uppy.off('error', this._onError)
 
     if (this.opts.importFromUploadURLs) {
       this.uppy.off('upload-success', this._onFileUploadURLAvailable)
     }
+
+    const { capabilities } = this.uppy.getState()
+    this.uppy.setState({
+      capabilities: {
+        ...capabilities,
+        individualCancellation: true
+      }
+    })
   }
 
   getAssembly (id) {
-    const state = this.getPluginState()
-    return state.assemblies[id]
+    const { assemblies } = this.getPluginState()
+    return assemblies[id]
   }
 
   getAssemblyFiles (assemblyID) {

+ 10 - 6
packages/@uppy/xhr-upload/src/index.js

@@ -507,10 +507,12 @@ module.exports = class XHRUpload extends Plugin {
 
   install () {
     if (this.opts.bundle) {
+      const { capabilities } = this.uppy.getState()
       this.uppy.setState({
-        capabilities: Object.assign({}, this.uppy.getState().capabilities, {
-          bundled: true
-        })
+        capabilities: {
+          ...capabilities,
+          individualCancellation: false
+        }
       })
     }
 
@@ -519,10 +521,12 @@ module.exports = class XHRUpload extends Plugin {
 
   uninstall () {
     if (this.opts.bundle) {
+      const { capabilities } = this.uppy.getState()
       this.uppy.setState({
-        capabilities: Object.assign({}, this.uppy.getState().capabilities, {
-          bundled: true
-        })
+        capabilities: {
+          ...capabilities,
+          individualCancellation: true
+        }
       })
     }