diff --git a/CHANGELOG.md b/CHANGELOG.md index 252707acd..3a155c910 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 user’s 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 can’t 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 can’t 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 diff --git a/src/core/Core.js b/src/core/Core.js index f004f97b0..c192b1749 100644 --- a/src/core/Core.js +++ b/src/core/Core.js @@ -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 / haven’t 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) } }) diff --git a/src/core/Utils.js b/src/core/Utils.js index 72068388a..ac64b3223 100644 --- a/src/core/Utils.js +++ b/src/core/Utils.js @@ -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, diff --git a/src/plugins/AwsS3/index.js b/src/plugins/AwsS3/index.js index 31f6af444..aab3b442e 100644 --- a/src/plugins/AwsS3/index.js +++ b/src/plugins/AwsS3/index.js @@ -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 diff --git a/src/plugins/Dashboard/Dashboard.js b/src/plugins/Dashboard/Dashboard.js index 53f6f3960..455d3cd3f 100644 --- a/src/plugins/Dashboard/Dashboard.js +++ b/src/plugins/Dashboard/Dashboard.js @@ -140,12 +140,12 @@ module.exports = function Dashboard (props) { type="button" onclick=${props.hideAllPanels}>${props.i18n('done')} - ${props.activePanel ? props.activePanel.render(props.state) : ''} + ${props.activePanel ? props.getPlugin(props.activePanel.id).render(props.state) : ''}
${props.progressindicators.map((target) => { - return target.render(props.state) + return props.getPlugin(target.id).render(props.state) })}
diff --git a/src/plugins/Dashboard/index.js b/src/plugins/Dashboard/index.js index 41e7f096c..6054adfae 100644 --- a/src/plugins/Dashboard/index.js +++ b/src/plugins/Dashboard/index.js @@ -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, diff --git a/src/plugins/ReduxDevTools.js b/src/plugins/ReduxDevTools.js new file mode 100644 index 000000000..9b8228936 --- /dev/null +++ b/src/plugins/ReduxDevTools.js @@ -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) + } + } +} diff --git a/src/plugins/XHRUpload.js b/src/plugins/XHRUpload.js index 1ccdd2a52..53e985e9c 100644 --- a/src/plugins/XHRUpload.js +++ b/src/plugins/XHRUpload.js @@ -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) => { diff --git a/test/acceptance/index.js b/test/acceptance/index.js index 9bdd55711..f5c295b71 100644 --- a/test/acceptance/index.js +++ b/test/acceptance/index.js @@ -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 + //
fallback test specificTests.fallback() // run all tests for all platforms