Преглед на файлове

Implement state managers.

Renée Kooi преди 7 години
родител
ревизия
972eb5b6c2
променени са 3 файла, в които са добавени 156 реда и са изтрити 37 реда
  1. 40 37
      src/core/Core.js
  2. 34 0
      src/store/DefaultStore.js
  3. 82 0
      src/store/ReduxStore.js

+ 40 - 37
src/core/Core.js

@@ -6,6 +6,7 @@ const cuid = require('cuid')
 const throttle = require('lodash.throttle')
 const prettyBytes = require('prettier-bytes')
 const match = require('mime-match')
+const DefaultStore = require('../store/DefaultStore')
 // const deepFreeze = require('deep-freeze-strict')
 
 /**
@@ -45,16 +46,13 @@ class Uppy {
       meta: {},
       onBeforeFileAdded: (currentFile, files) => Promise.resolve(),
       onBeforeUpload: (files, done) => Promise.resolve(),
-      locale: defaultLocale
+      locale: defaultLocale,
+      store: new DefaultStore()
     }
 
     // Merge default options with the ones set by user
     this.opts = Object.assign({}, defaultOptions, opts)
 
-    // // Dictates in what order different plugin types are ran:
-    // this.types = [ 'presetter', 'orchestrator', 'progressindicator',
-    //                 'acquirer', 'modifier', 'uploader', 'presenter', 'debugger']
-
     this.locale = Object.assign({}, defaultLocale, this.opts.locale)
     this.locale.strings = Object.assign({}, defaultLocale.strings, this.opts.locale.strings)
 
@@ -97,7 +95,8 @@ class Uppy {
     this.uploaders = []
     this.postProcessors = []
 
-    this.state = {
+    this.store = this.opts.store
+    this.setState({
       plugins: {},
       files: {},
       capabilities: {
@@ -110,7 +109,12 @@ class Uppy {
         type: 'info',
         message: ''
       }
-    }
+    })
+
+    this._storeUnsubscribe = this.store.subscribe((prevState, nextState, patch) => {
+      this.emit('core:state-update', prevState, nextState, patch)
+      this.updateAll(nextState)
+    })
 
     // for debugging and testing
     // this.updateNum = 0
@@ -137,23 +141,19 @@ class Uppy {
    * @param {patch} object
    */
   setState (patch) {
-    const prevState = Object.assign({}, this.state)
-    const nextState = Object.assign({}, this.state, patch)
-
-    this.state = nextState
-    this.emit('core:state-update', prevState, nextState, patch)
-
-    this.updateAll(this.state)
+    this.store.setState(patch)
   }
 
   /**
    * Returns current state
-   *
    */
   getState () {
-    // use deepFreeze for debugging
-    // return deepFreeze(this.state)
-    return this.state
+    return this.store.getState()
+  }
+
+  // Back compat.
+  get state () {
+    return this.getState()
   }
 
   resetProgress () {
@@ -163,7 +163,7 @@ class Uppy {
       uploadComplete: false,
       uploadStarted: false
     }
-    const files = Object.assign({}, this.state.files)
+    const files = Object.assign({}, this.getState().files)
     const updatedFiles = {}
     Object.keys(files).forEach(fileID => {
       const updatedFile = Object.assign({}, files[fileID])
@@ -214,14 +214,14 @@ class Uppy {
   }
 
   setMeta (data) {
-    const newMeta = Object.assign({}, this.state.meta, data)
+    const newMeta = Object.assign({}, this.getState().meta, data)
     this.log('Adding metadata:')
     this.log(data)
     this.setState({meta: newMeta})
   }
 
   updateMeta (data, fileID) {
-    const updatedFiles = Object.assign({}, this.state.files)
+    const updatedFiles = Object.assign({}, this.getState().files)
     if (!updatedFiles[fileID]) {
       this.log('Was trying to set metadata for a file that’s not with us anymore: ', fileID)
       return
@@ -241,7 +241,7 @@ class Uppy {
   */
   checkMinNumberOfFiles () {
     const {minNumberOfFiles} = this.opts.restrictions
-    if (Object.keys(this.state.files).length < minNumberOfFiles) {
+    if (Object.keys(this.getState().files).length < minNumberOfFiles) {
       this.info(`${this.i18n('youHaveToAtLeastSelectX', {smart_count: minNumberOfFiles})}`, 'error', 5000)
       return false
     }
@@ -260,7 +260,7 @@ class Uppy {
     const {maxFileSize, maxNumberOfFiles, allowedFileTypes} = this.opts.restrictions
 
     if (maxNumberOfFiles) {
-      if (Object.keys(this.state.files).length + 1 > maxNumberOfFiles) {
+      if (Object.keys(this.getState().files).length + 1 > maxNumberOfFiles) {
         this.info(`${this.i18n('youCanOnlyUploadX', {smart_count: maxNumberOfFiles})}`, 'error', 5000)
         return false
       }
@@ -304,7 +304,7 @@ class Uppy {
       return Promise.reject(new Error(`onBeforeFileAdded: ${message}`))
     }).then(() => {
       return Utils.getFileType(file).then((fileType) => {
-        const updatedFiles = Object.assign({}, this.state.files)
+        const updatedFiles = Object.assign({}, this.getState().files)
         let fileName
         if (file.name) {
           fileName = file.name
@@ -406,7 +406,7 @@ class Uppy {
    * Set the preview URL for a file.
    */
   setPreviewURL (fileID, preview) {
-    const { files } = this.state
+    const { files } = this.getState()
     this.setState({
       files: Object.assign({}, files, {
         [fileID]: Object.assign({}, files[fileID], {
@@ -498,7 +498,7 @@ class Uppy {
   }
 
   retryUpload (fileID) {
-    const updatedFiles = Object.assign({}, this.state.files)
+    const updatedFiles = Object.assign({}, this.getState().files)
     const updatedFile = Object.assign({}, updatedFiles[fileID],
       { error: null, isPaused: false }
     )
@@ -598,14 +598,14 @@ class Uppy {
     })
 
     this.on('core:upload-error', (fileID, error) => {
-      const updatedFiles = Object.assign({}, this.state.files)
+      const updatedFiles = Object.assign({}, this.getState().files)
       const updatedFile = Object.assign({}, updatedFiles[fileID],
         { error: error.message }
       )
       updatedFiles[fileID] = updatedFile
       this.setState({ files: updatedFiles, error: error.message })
 
-      const fileName = this.state.files[fileID].name
+      const fileName = this.getState().files[fileID].name
       let message = `Failed to upload ${fileName}`
       if (typeof error === 'object' && error.message) {
         message = { message: message, details: error.message }
@@ -852,6 +852,8 @@ class Uppy {
       this.devToolsUnsubscribe()
     }
 
+    this._storeUnsubscribe()
+
     this.iteratePlugins((plugin) => {
       plugin.uninstall()
     })
@@ -893,7 +895,7 @@ class Uppy {
   }
 
   hideInfo () {
-    const newInfo = Object.assign({}, this.state.info, {
+    const newInfo = Object.assign({}, this.getState().info, {
       isHidden: true
     })
     this.setState({
@@ -961,7 +963,7 @@ class Uppy {
   restore (uploadID) {
     this.log(`Core: attempting to restore upload "${uploadID}"`)
 
-    if (!this.state.currentUploads[uploadID]) {
+    if (!this.getState().currentUploads[uploadID]) {
       this.removeUpload(uploadID)
       return Promise.reject(new Error('Nonexistent upload'))
     }
@@ -984,7 +986,7 @@ class Uppy {
     })
 
     this.setState({
-      currentUploads: Object.assign({}, this.state.currentUploads, {
+      currentUploads: Object.assign({}, this.getState().currentUploads, {
         [uploadID]: {
           fileIDs: fileIDs,
           step: 0
@@ -1001,7 +1003,7 @@ class Uppy {
    * @param {string} uploadID The ID of the upload.
    */
   removeUpload (uploadID) {
-    const currentUploads = Object.assign({}, this.state.currentUploads)
+    const currentUploads = Object.assign({}, this.getState().currentUploads)
     delete currentUploads[uploadID]
 
     this.setState({
@@ -1015,7 +1017,7 @@ class Uppy {
    * @private
    */
   runUpload (uploadID) {
-    const uploadData = this.state.currentUploads[uploadID]
+    const uploadData = this.getState().currentUploads[uploadID]
     const fileIDs = uploadData.fileIDs
     const restoreStep = uploadData.step
 
@@ -1032,11 +1034,12 @@ class Uppy {
       }
 
       lastStep = lastStep.then(() => {
-        const currentUpload = Object.assign({}, this.state.currentUploads[uploadID], {
+        const { currentUploads } = this.getState()
+        const currentUpload = Object.assign({}, currentUploads[uploadID], {
           step: step
         })
         this.setState({
-          currentUploads: Object.assign({}, this.state.currentUploads, {
+          currentUploads: Object.assign({}, currentUploads, {
             [uploadID]: currentUpload
           })
         })
@@ -1085,7 +1088,7 @@ class Uppy {
     }
 
     const beforeUpload = Promise.resolve()
-      .then(() => this.opts.onBeforeUpload(this.state.files))
+      .then(() => this.opts.onBeforeUpload(this.getState().files))
 
     return beforeUpload.catch((err) => {
       const message = typeof err === 'object' ? err.message : err
@@ -1093,7 +1096,7 @@ class Uppy {
       return Promise.reject(new Error(`onBeforeUpload: ${message}`))
     }).then(() => {
       const waitingFileIDs = []
-      Object.keys(this.state.files).forEach((fileID) => {
+      Object.keys(this.getState().files).forEach((fileID) => {
         const file = this.getFile(fileID)
 
         if (!file.progress.uploadStarted || file.isRemote) {

+ 34 - 0
src/store/DefaultStore.js

@@ -0,0 +1,34 @@
+module.exports = class DefaultStore {
+  constructor () {
+    this.state = {}
+    this.callbacks = []
+  }
+
+  getState () {
+    return this.state
+  }
+
+  setState (patch) {
+    const prevState = Object.assign({}, this.state)
+    const nextState = Object.assign({}, this.state, patch)
+
+    this.state = nextState
+    this._publish(prevState, nextState, patch)
+  }
+
+  subscribe (listener) {
+    this.callbacks.push(listener)
+    return () => {
+      this.callbacks.splice(
+        this.callbacks.indexOf(listener),
+        1
+      )
+    }
+  }
+
+  _publish (...args) {
+    this.callbacks.forEach((listener) => {
+      listener(...args)
+    })
+  }
+}

+ 82 - 0
src/store/ReduxStore.js

@@ -0,0 +1,82 @@
+const cuid = require('cuid')
+
+// Redux action name.
+const STATE_UPDATE = 'uppy/STATE_UPDATE'
+
+// Pluck Uppy state from the Redux store in the default location.
+const defaultSelector = (id) => (state) => state.uppy[id]
+
+/**
+ * Redux store.
+ *
+ * @param {object} opts.store - The Redux store to use.
+ * @param {string} opts.id - This store instance's ID. Defaults to a random string.
+ *    If you need to access Uppy state through Redux, eg. to render custom UI, set this to something constant.
+ * @param {function} opts.selector - Function, `(state) => uppyState`, to pluck state from the Redux store.
+ *    Defaults to retrieving `state.uppy[opts.id]`. Override if you placed Uppy state elsewhere in the Redux store.
+ */
+class ReduxStore {
+  constructor (opts) {
+    this._store = opts.store
+    this._id = opts.id || cuid()
+    this._selector = opts.selector || defaultSelector(this._id)
+  }
+
+  setState (patch) {
+    this._store.dispatch({
+      type: STATE_UPDATE,
+      id: this._id,
+      payload: patch
+    })
+  }
+
+  getState () {
+    return this._selector(this._store.getState())
+  }
+
+  subscribe (cb) {
+    let prevState = this.getState()
+    return this._store.subscribe(() => {
+      const nextState = this.getState()
+      if (prevState !== nextState) {
+        const patch = getPatch(prevState, nextState)
+        cb(prevState, nextState, patch)
+        prevState = nextState
+      }
+    })
+  }
+}
+
+function getPatch (prev, next) {
+  const nextKeys = Object.keys(next)
+  const patch = {}
+  nextKeys.forEach((k) => {
+    if (prev[k] !== next[k]) patch[k] = next[k]
+  })
+  return patch
+}
+
+function reducer (state = {}, action) {
+  if (action.type === STATE_UPDATE) {
+    const newState = Object.assign({}, state[action.id], action.payload)
+    return Object.assign({}, state, {
+      [action.id]: newState
+    })
+  }
+  return state
+}
+
+function middleware () {
+  // Do nothing, at the moment.
+  return () => (next) => (action) => {
+    next(action)
+  }
+}
+
+module.exports = function createReduxStore (opts) {
+  return new ReduxStore(opts)
+}
+
+module.exports.STATE_UPDATE = STATE_UPDATE
+module.exports.reducer = reducer
+module.exports.middleware = middleware