Merge branch 'master' into feature/upload-retry

This commit is contained in:
Artur Paikin 2017-10-03 10:18:51 -04:00
commit 97cd38798b
9 changed files with 231 additions and 65 deletions

View file

@ -83,8 +83,9 @@ What we need to do to release Uppy 1.0
To be released: 2017-10-27
- [ ] webcam: look into simplifying / improving webcam plugin, only showing the tab when webcam is available (@arturi, @goto-bus-stop)
- [ ] provider: improve UI, add icons for file types? (@arturi)
- [ ] webcam: look into simplifying / improving webcam plugin (probably good to do modern browsers only), only showing the tab when webcam is available (@arturi, @goto-bus-stop)
- [ ] core: Redux PR (#216, #338 / @arturi, @goto-bus-stop, @richardwillars)
- [ ] provider: improve UI, add icons for file types (@arturi)
- [ ] core: css-in-js, while keeping non-random classnames (ideally prefixed) and useful preprocessor features. also see simple https://github.com/codemirror/CodeMirror/blob/master/lib/codemirror.css (@arturi, @goto-bus-stop)
- [ ] core: research !important styles to be immune to any environment/page. Maybe use smth like `postcss-safe-important`. Or increase specificity (with .Uppy) (@arturi)
- [ ] test: add https://github.com/pa11y/pa11y for automated accessibility testing (@arturi)
@ -92,30 +93,34 @@ To be released: 2017-10-27
- [ ] core: allow setting custom `id` for plugins: https://github.com/transloadit/uppy/pull/328#issuecomment-328242214 (@arturi)
- [ ] add `FormEncapsulator`: a plugin that is used in conjunction with any other acquirer, responsible for injecting any result (like from Transloadit plugin) back into the form (jquery-sdk includes the whole Assembly Status JSON in a hidden field i think)
- [ ] dashboard: allow minimizing the Dashboard during upload (Uppy then becomes just a tiny progress indicator) (@arturi)
- [ ] dashboard: cancel button for any kind of uploads? currently resume/pause only for tus, and cancel for XHR (@arturi, @goto-bus-stop)
- [ ] goldenretriever: Ability to clear upload history or set expiry date (#324 / @arturi)
- [ ] core: return `{ successful, failed }` from `uppy.upload()`
- [ ] core: return `{ successful, failed }` from `uppy.upload()` (@goto-bus-stop)
- [ ] core: refactor `uppy-base` (@arturi)
- [ ] uppy-server: look into storing tokens in users browser only (@ifedapoolarewaju)
- [ ] plugins: add tabindex="0" to buttons and tabs?
- [ ] goldenretriever: add “ghost” files (@arturi)
# next
## 0.20.0
To be released: 2017-09-29.
To be released: 2017-10-02.
Theme: React and Retry
- [ ] core: Redux PR (#216, #338 / @arturi, @goto-bus-stop, @richardwillars)
- [ ] dashboard: cancel button for any kind of uploads? currently resume/pause only for tus, and cancel for XHR (@arturi, @goto-bus-stop)
- [ ] core: show error and offer retry when upload cant start and/or fails (offline, wrong endpoint) — now it just sits there; add error in file progress state, UI, question mark button (#307 / @arturi)
- [ ] core: retry/error when upload cant start or fails (offline, connection lost, wrong endpoint); add error in file progress state, UI, question mark button (#307 / @arturi)
- [ ] core: support for retry in Tus plugin (#307 / @arturi)
- [ ] core: support for retry in XHRUpload plugin (#307 / @arturi)
- [x] core: Add support for Redux DevTools via a plugin (#373 / @arturi)
- [x] core: improve and merge the React PR (#170 / @goto-bus-stop, @arturi)
- [ ] goldenretriever: add “ghost” files (@arturi)
- [x] core: improve core.log method, add timestamps (#372 / @arturi)
- [x] uploaders: upload resolution changes, followup to #323 (#347 / @goto-bus-stop)
- [ ] uploaders: issue warning when no uploading plugins are used
- [ ] core: refactor `uppy-base`
- [x] uploaders: issue warning when no uploading plugins are used (#372 / @arturi)
- [x] core: fix `replaceTargetContent` and add tests for `Plugin` (#354 / @gavboulton)
- [x] goldenretriever: Omit completed uploads from saved file state—previously, when an upload was finished and the user refreshed the page, all the finished files would still be there because we saved the entire list of files. Changed this to only store files that are part of an in-progress upload, or that have yet to be uploaded (#358, #324 / @goto-bus-stop)
- [x] goldenretriever: Remove files from cache when upload finished—this uses the deleteBlobs function when core:success fires (#358, #324 / @goto-bus-stop)
- [ ] goldenretriever: add a timestamp to cached blobs, and to delete old blobs on boot (#358, #324 / @goto-bus-stop)
- [ ] s3: have some way to configure content-disposition for uploads, see #243 (@goto-bus-stop)
- [x] s3: have some way to configure content-disposition for uploads, see #243 (@goto-bus-stop)
- [x] core: move `setPluginState` and add `getPluginState` to `Plugin` class (#363 / @goto-bus-stop)
## 0.19.1

View file

@ -65,10 +65,10 @@ class Uppy {
// Container for different types of plugins
this.plugins = {}
// @TODO maybe bindall
this.translator = new Translator({locale: this.opts.locale})
this.i18n = this.translator.translate.bind(this.translator)
this.getState = this.getState.bind(this)
this.getPlugin = this.getPlugin.bind(this)
this.updateMeta = this.updateMeta.bind(this)
this.initSocket = this.initSocket.bind(this)
this.log = this.log.bind(this)
@ -112,7 +112,35 @@ class Uppy {
}
// for debugging and testing
this.updateNum = 0
// // Implement monitors actions.
// // See https://medium.com/@zalmoxis/redux-devtools-without-redux-or-how-to-have-a-predictable-state-with-any-architecture-61c5f5a7716f
// // and https://github.com/zalmoxisus/mobx-remotedev/blob/master/src/monitorActions.js
// this.withDevTools = typeof window !== 'undefined' && window.__REDUX_DEVTOOLS_EXTENSION__
// if (this.withDevTools) {
// this.devTools = window.devToolsExtension.connect()
// this.devToolsUnsubscribe = this.devTools.subscribe((message) => {
// if (message.type === 'DISPATCH') {
// console.log(message.payload.type)
// switch (message.payload.type) {
// case 'RESET':
// this.reset()
// return
// case 'IMPORT_STATE':
// const computedStates = message.payload.nextLiftedState.computedStates
// this.state = Object.assign({}, this.state, computedStates[computedStates.length - 1].state)
// this.updateAll(this.state)
// return
// case 'JUMP_TO_STATE':
// case 'JUMP_TO_ACTION':
// // this.setState(state)
// this.state = Object.assign({}, this.state, JSON.parse(message.state))
// this.updateAll(this.state)
// }
// }
// })
// }
// this.updateNum = 0
if (this.opts.debug) {
global.UppyState = this.state
global.uppyLog = ''
@ -576,6 +604,12 @@ class Uppy {
// this.setState({bla: 'bla'})
// }, 20)
// this.on('core:state-update', (prevState, nextState, patch) => {
// if (this.withDevTools) {
// this.devTools.send('UPPY_STATE_UPDATE', nextState)
// }
// })
this.on('core:error', (error) => {
this.setState({ error })
})
@ -650,7 +684,6 @@ class Uppy {
const throttledCalculateProgress = throttle(this.calculateProgress, 100, {leading: true, trailing: false})
this.on('core:upload-progress', (data) => {
// this.calculateProgress(data)
throttledCalculateProgress(data)
})
@ -659,8 +692,6 @@ class Uppy {
const updatedFile = Object.assign({}, updatedFiles[fileID], {
progress: Object.assign({}, updatedFiles[fileID].progress, {
uploadComplete: true,
// good or bad idea? setting the percentage to 100 if upload is successful,
// so that if we lost some progress events on the way, its still marked “compete”?
percentage: 100
}),
uploadURL: uploadURL,
@ -848,6 +879,10 @@ class Uppy {
close () {
this.reset()
if (this.withDevTools) {
this.devToolsUnsubscribe()
}
this.iteratePlugins((plugin) => {
plugin.uninstall()
})
@ -901,25 +936,35 @@ class Uppy {
/**
* Logs stuff to console, only if `debug` is set to true. Silent in production.
*
* @return {String|Object} to log
* @param {String|Object} msg to log
* @param {String} type optional `error` or `warning`
*/
log (msg, type) {
if (!this.opts.debug) {
return
}
let message = `[Uppy] [${Utils.getTimeStamp()}] ${msg}`
global.uppyLog = global.uppyLog + '\n' + 'DEBUG LOG: ' + msg
if (type === 'error') {
console.error(`LOG: ${msg}`)
console.error(message)
return
}
if (type === 'warning') {
console.warn(message)
return
}
if (msg === `${msg}`) {
console.log(`LOG: ${msg}`)
console.log(message)
} else {
message = `[Uppy] [${Utils.getTimeStamp()}]`
console.log(message)
console.dir(msg)
}
global.uppyLog = global.uppyLog + '\n' + 'DEBUG LOG: ' + msg
}
initSocket (opts) {
@ -1052,7 +1097,11 @@ class Uppy {
*
* @return {Promise}
*/
upload (forceUpload) {
upload () {
if (!this.plugins.uploader) {
this.log('No uploader type plugins are used', 'warning')
}
const isMinNumberOfFilesReached = this.checkMinNumberOfFiles()
if (!isMinNumberOfFilesReached) {
return Promise.reject(new Error('Minimum number of files has not been reached'))
@ -1070,15 +1119,7 @@ class Uppy {
Object.keys(this.state.files).forEach((fileID) => {
const file = this.getFile(fileID)
// TODO: replace files[file].isRemote with some logic
//
// filter files that are now yet being uploaded / havent been uploaded
// and remote too
if (forceUpload) {
this.resetProgress()
waitingFileIDs.push(file.id)
} else if (!file.progress.uploadStarted || file.isRemote) {
if (!file.progress.uploadStarted || file.isRemote) {
waitingFileIDs.push(file.id)
}
})

View file

@ -41,6 +41,24 @@ function toArray (list) {
return Array.prototype.slice.call(list || [], 0)
}
/**
* Returns a timestamp in the format of `hours:minutes:seconds`
*/
function getTimeStamp () {
var date = new Date()
var hours = pad(date.getHours().toString())
var minutes = pad(date.getMinutes().toString())
var seconds = pad(date.getSeconds().toString())
return hours + ':' + minutes + ':' + seconds
}
/**
* Adds zero to strings shorter than two characters
*/
function pad (str) {
return str.length !== 2 ? 0 + str : str
}
/**
* Takes a file object and turns it into fileID, by converting file.name to lowercase,
* removing extra characters and adding type, size and lastModified
@ -519,6 +537,7 @@ function settle (promises) {
module.exports = {
generateFileID,
toArray,
getTimeStamp,
runPromiseSequence,
supportsMediaRecorder,
isTouchDevice,

View file

@ -99,17 +99,24 @@ module.exports = class AwsS3 extends Plugin {
const {
method = 'post',
url,
fields
fields,
headers
} = responses[index]
const xhrOpts = {
method,
formData: method.toLowerCase() === 'post',
endpoint: url,
fieldName: 'file',
metaFields: Object.keys(fields)
}
if (headers) {
xhrOpts.headers = headers
}
const updatedFile = Object.assign({}, file, {
meta: Object.assign({}, file.meta, fields),
xhrUpload: {
method,
formData: method.toLowerCase() === 'post',
endpoint: url,
fieldName: 'file',
metaFields: Object.keys(fields)
}
xhrUpload: xhrOpts
})
updatedFiles[id] = updatedFile

View file

@ -140,12 +140,12 @@ module.exports = function Dashboard (props) {
type="button"
onclick=${props.hideAllPanels}>${props.i18n('done')}</button>
</div>
${props.activePanel ? props.activePanel.render(props.state) : ''}
${props.activePanel ? props.getPlugin(props.activePanel.id).render(props.state) : ''}
</div>
<div class="UppyDashboard-progressindicators">
${props.progressindicators.map((target) => {
return target.render(props.state)
return props.getPlugin(target.id).render(props.state)
})}
</div>

View file

@ -93,7 +93,7 @@ module.exports = class DashboardUI extends Plugin {
addTarget (plugin) {
const callerPluginId = plugin.id || plugin.constructor.name
const callerPluginName = plugin.title || callerPluginId
const callerPluginIcon = plugin.icon || this.opts.defaultTabIcon
// const callerPluginIcon = plugin.icon || this.opts.defaultTabIcon
const callerPluginType = plugin.type
if (callerPluginType !== 'acquirer' &&
@ -107,9 +107,9 @@ module.exports = class DashboardUI extends Plugin {
const target = {
id: callerPluginId,
name: callerPluginName,
icon: callerPluginIcon,
// icon: callerPluginIcon,
type: callerPluginType,
render: plugin.render,
// render: plugin.render,
isHidden: true
}
@ -306,11 +306,17 @@ module.exports = class DashboardUI extends Plugin {
totalSize = prettyBytes(totalSize)
totalUploadedSize = prettyBytes(totalUploadedSize)
const acquirers = pluginState.targets.filter((target) => {
const acquirers = pluginState.targets.filter(target => {
const plugin = this.core.getPlugin(target.id)
target.icon = plugin.icon || this.opts.defaultTabIcon
target.render = plugin.render
return target.type === 'acquirer'
})
const progressindicators = pluginState.targets.filter((target) => {
const progressindicators = pluginState.targets.filter(target => {
const plugin = this.core.getPlugin(target.id)
target.icon = plugin.icon || this.opts.defaultTabIcon
target.render = plugin.render
return target.type === 'progressindicator'
})
@ -348,6 +354,7 @@ module.exports = class DashboardUI extends Plugin {
totalProgress: state.totalProgress,
acquirers: acquirers,
activePanel: pluginState.activePanel,
getPlugin: this.core.getPlugin,
progressindicators: progressindicators,
autoProceed: this.core.opts.autoProceed,
hideUploadButton: this.opts.hideUploadButton,

View file

@ -0,0 +1,69 @@
const Plugin = require('./Plugin')
/**
* Add Redux DevTools support to Uppy
*
* See https://medium.com/@zalmoxis/redux-devtools-without-redux-or-how-to-have-a-predictable-state-with-any-architecture-61c5f5a7716f
* and https://github.com/zalmoxisus/mobx-remotedev/blob/master/src/monitorActions.js
*/
module.exports = class ReduxDevTools extends Plugin {
constructor (core, opts) {
super(core, opts)
this.type = 'debugger'
this.id = 'ReduxDevTools'
this.title = 'Redux DevTools'
// set default options
const defaultOptions = {}
// merge default options with the ones set by user
this.opts = Object.assign({}, defaultOptions, opts)
this.handleStateChange = this.handleStateChange.bind(this)
this.initDevTools = this.initDevTools.bind(this)
}
handleStateChange (prevState, nextState, patch) {
this.devTools.send('UPPY_STATE_UPDATE', nextState)
}
initDevTools () {
this.devTools = window.devToolsExtension.connect()
this.devToolsUnsubscribe = this.devTools.subscribe((message) => {
if (message.type === 'DISPATCH') {
console.log(message.payload.type)
// Implement monitors actions
switch (message.payload.type) {
case 'RESET':
this.core.reset()
return
case 'IMPORT_STATE':
const computedStates = message.payload.nextLiftedState.computedStates
this.core.state = Object.assign({}, this.core.state, computedStates[computedStates.length - 1].state)
this.core.updateAll(this.core.state)
return
case 'JUMP_TO_STATE':
case 'JUMP_TO_ACTION':
// this.setState(state)
this.core.state = Object.assign({}, this.core.state, JSON.parse(message.state))
this.core.updateAll(this.core.state)
}
}
})
}
install () {
this.withDevTools = typeof window !== 'undefined' && window.__REDUX_DEVTOOLS_EXTENSION__
if (this.withDevTools) {
this.initDevTools()
this.core.on('core:state-update', this.handleStateChange)
}
}
uninstall () {
if (this.withDevTools) {
this.core.emitter.off('core:state-update', this.handleStateUpdate)
}
}
}

View file

@ -38,6 +38,24 @@ module.exports = class XHRUpload extends Plugin {
this.handleRetryAll = this.handleRetryAll.bind(this)
}
getOptions (file) {
const opts = Object.assign({},
this.opts,
this.core.state.xhrUpload || {},
file.xhrUpload || {}
)
opts.headers = {}
Object.assign(opts.headers, this.opts.headers)
if (this.core.state.xhrUpload) {
Object.assign(opts.headers, this.core.state.xhrUpload.headers)
}
if (file.xhrUpload) {
Object.assign(opts.headers, file.xhrUpload.headers)
}
return opts
}
createFormDataUpload (file, opts) {
const formPost = new FormData()
@ -59,11 +77,7 @@ module.exports = class XHRUpload extends Plugin {
}
upload (file, current, total) {
const opts = Object.assign({},
this.opts,
this.core.state.xhrUpload || {},
file.xhrUpload || {}
)
const opts = this.getOptions(file)
this.core.log(`uploading ${current} of ${total}`)
return new Promise((resolve, reject) => {
@ -102,14 +116,6 @@ module.exports = class XHRUpload extends Plugin {
this.core.emit('core:upload-error', file.id, error)
return reject(error)
}
// var upload = {}
//
// if (opts.bundle) {
// upload = {files: files}
// } else {
// upload = {file: files[current]}
// }
})
xhr.addEventListener('error', (ev) => {
@ -143,10 +149,20 @@ module.exports = class XHRUpload extends Plugin {
}
uploadRemote (file, current, total) {
const opts = Object.assign({}, this.opts, file.xhrUpload || {})
const opts = this.getOptions(file)
return new Promise((resolve, reject) => {
this.core.emit('core:upload-started', file.id)
const fields = {}
const metaFields = Array.isArray(opts.metaFields)
? opts.metaFields
// Send along all fields by default.
: Object.keys(file.meta)
metaFields.forEach((name) => {
fields[name] = file.meta.name
})
fetch(file.remote.url, {
method: 'post',
credentials: 'include',
@ -157,7 +173,9 @@ module.exports = class XHRUpload extends Plugin {
body: JSON.stringify(Object.assign({}, file.remote.body, {
endpoint: opts.endpoint,
size: file.data.size,
fieldname: opts.fieldName
fieldname: opts.fieldName,
fields,
headers: opts.headers
}))
})
.then((res) => {

View file

@ -52,7 +52,7 @@ var platforms = [
{ browser: 'Firefox', version: '38.0', os: 'Linux' },
{ browser: 'Internet Explorer', version: '10.0', os: 'Windows 8' },
{ browser: 'Internet Explorer', version: '11.103', os: 'Windows 10' },
{ browser: 'Chrome', version: '48.0', os: 'Windows XP' },
{ browser: 'Chrome', version: '48.0', os: 'Windows 7' },
{ browser: 'Firefox', version: '34.0', os: 'Windows 7' }
]
@ -100,8 +100,8 @@ function buildDriver (platform) {
var specificTests = {
fallback: function () {
var ancientPlatform = { browser: 'internet explorer', version: '6.0', os: 'Windows XP' }
var driver = buildDriver({ browser: 'internet explorer', version: '6.0', os: 'Windows XP' })
var ancientPlatform = { browser: 'internet explorer', version: '8.0', os: 'Windows 7' }
var driver = buildDriver({ browser: 'internet explorer', version: '8.0', os: 'Windows 7' })
var test = require('./fallback.spec.js')
test(driver, ancientPlatform, host)
@ -113,7 +113,7 @@ var specificTests = {
function runAllTests () {
if (isRemoteTest) {
// run custom platform-specific tests here
// fallback test
// <form> fallback test
specificTests.fallback()
// run all tests for all platforms