Kaynağa Gözat

Merge pull request #826 from transloadit/feature/remote-late-add

 Wait for user to press Done in provider view before adding files.
Artur Paikin 6 yıl önce
ebeveyn
işleme
d8fea4bb40

+ 1 - 1
CHANGELOG.md

@@ -122,7 +122,7 @@ To Be Released: 2018-05-31.
 - [ ] uppy-server: document docker image setup for uppy-server (@ifedapoolarewaju)
 - [ ] xhrupload: emit a final `upload-progress` event in the XHRUpload plugin just before firing `upload-complete` (tus-js-client already handles this internally) (@arturi)
 - [x] core: add more mime-to-extension mappings from https://github.com/micnic/mime.json/blob/master/index.json (which ones?) (#806 /@arturi, @goto-bus-stop)
-- [ ] providers: select files only after “select” is pressed, don’t add them right away when they are checked (keep a list of fileIds in state?); better UI + solves issue with autoProceed uploading in background, which is weird; re-read https://github.com/transloadit/uppy/pull/419#issuecomment-345210519 (@arturi, @goto-bus-stop)
+- [x] providers: select files only after “select” is pressed, don’t add them right away when they are checked (keep a list of fileIds in state?); better UI + solves issue with autoProceed uploading in background, which is weird; re-read https://github.com/transloadit/uppy/pull/419#issuecomment-345210519 (@arturi, @goto-bus-stop)
 - [x] tus: add `filename` and `filetype`, so that tus servers knows what headers to set  https://github.com/tus/tus-js-client/commit/ebc5189eac35956c9f975ead26de90c896dbe360 (#844 / @vith)
 - [ ] core: look into utilizing https://github.com/que-etc/resize-observer-polyfill for responsive components. See also https://github.com/transloadit/uppy/issues/750
 - [x] core: ⚠️ **breaking** removed .run() (to solve issues like #756), update docs (#793 / goto-bus-stop)

+ 8 - 1
src/core/Core.js

@@ -34,7 +34,14 @@ class Uppy {
         failedToUpload: 'Failed to upload %{file}',
         noInternetConnection: 'No Internet connection',
         connectedToInternet: 'Connected to the Internet',
-        noFilesFound: 'You have no files or folders here'
+        // Strings for remote providers
+        noFilesFound: 'You have no files or folders here',
+        selectXFiles: {
+          0: 'Select %{smart_count} file',
+          1: 'Select %{smart_count} files'
+        },
+        cancel: 'Cancel',
+        logOut: 'Log out'
       }
     }
 

+ 9 - 15
src/scss/_provider.scss

@@ -392,21 +392,15 @@
   outline: rgb(59, 153, 252) auto 5px;
 }
 
-.uppy-ProviderBrowser-doneBtn {
-  position: absolute;
-  bottom: 16px;
-  right: 16px;
-  z-index: $zIndex-3;
-  width: 50px;
-  height: 50px;
+.uppy-ProviderBrowser-footer {
+  display: flex;
+  align-items: center;
+  background: $color-white;
+  height: 65px;
+  border-top: 1px solid rgba($color-gray, 0.3);
+  padding: 0 15px;
 
-  .uppy-Dashboard--wide & {
-    width: 60px;
-    height: 60px;
+  & button {
+    margin-right: 10px;
   }
 }
-
-.uppy-ProviderBrowser-doneBtn .UppyIcon {
-  width: 45%;
-  height: 45%;
-}

+ 36 - 42
src/views/ProviderView/Browser.js

@@ -1,9 +1,11 @@
+const classNames = require('classnames')
 const Breadcrumbs = require('./Breadcrumbs')
 const Filter = require('./Filter')
 const Table = require('./ItemList')
+const FooterActions = require('./FooterActions')
 const { h } = require('preact')
 
-module.exports = (props) => {
+const Browser = (props) => {
   let filteredFolders = props.folders
   let filteredFiles = props.files
 
@@ -12,56 +14,48 @@ module.exports = (props) => {
     filteredFiles = props.filterItems(props.files)
   }
 
+  const selected = props.currentSelection.length
+
   return (
-    <div class={`uppy-ProviderBrowser uppy-ProviderBrowser-viewType--${props.viewType}`}>
+    <div class={classNames('uppy-ProviderBrowser', `uppy-ProviderBrowser-viewType--${props.viewType}`)}>
       <div class="uppy-ProviderBrowser-header">
-        <div class={`uppy-ProviderBrowser-headerBar ${!props.showBreadcrumbs ? 'uppy-ProviderBrowser-headerBar--simple' : ''}`}>
+        <div class={classNames('uppy-ProviderBrowser-headerBar', !props.showBreadcrumbs && 'uppy-ProviderBrowser-headerBar--simple')}>
           <div class="uppy-Provider-breadcrumbsIcon">{props.pluginIcon && props.pluginIcon()}</div>
-          {props.showBreadcrumbs && Breadcrumbs({
-            getFolder: props.getFolder,
-            directories: props.directories,
-            title: props.title
-          })}
-          <span class="uppy-ProviderBrowser-user">{props.username}</span>
-          <button type="button" onclick={props.logout} class="uppy-ProviderBrowser-userLogout">Log out</button>
+          {props.showBreadcrumbs && <Breadcrumbs
+            getFolder={props.getFolder}
+            directories={props.directories}
+            title={props.title} />
+          }
+          <button type="button" onclick={props.logout} class="uppy-ProviderBrowser-userLogout">
+            {props.i18n('logOut')}
+          </button>
         </div>
       </div>
       { props.showFilter && <Filter {...props} /> }
-      {Table({
-        columns: [{
+      <Table
+        columns={[{
           name: 'Name',
           key: 'title'
-        }],
-        folders: filteredFolders,
-        files: filteredFiles,
-        activeRow: props.isActiveRow,
-        sortByTitle: props.sortByTitle,
-        sortByDate: props.sortByDate,
-        handleFileClick: props.addFile,
-        handleFolderClick: props.getNextFolder,
-        isChecked: props.isChecked,
-        toggleCheckbox: props.toggleCheckbox,
-        getItemName: props.getItemName,
-        getItemIcon: props.getItemIcon,
-        handleScroll: props.handleScroll,
-        title: props.title,
-        showTitles: props.showTitles,
-        getItemId: props.getItemId,
-        i18n: props.i18n
-      })}
-      <button class="UppyButton--circular UppyButton--blue uppy-ProviderBrowser-doneBtn"
-        type="button"
-        aria-label="Done picking files"
-        title="Done picking files"
-        onclick={props.done}>
-        <svg aria-hidden="true" class="UppyIcon" width="13px" height="9px" viewBox="0 0 13 9">
-          <polygon points="5 7.293 1.354 3.647 0.646 4.354 5 8.707 12.354 1.354 11.646 0.647" />
-        </svg>
-      </button>
+        }]}
+        folders={filteredFolders}
+        files={filteredFiles}
+        activeRow={props.isActiveRow}
+        sortByTitle={props.sortByTitle}
+        sortByDate={props.sortByDate}
+        isChecked={props.isChecked}
+        handleFolderClick={props.getNextFolder}
+        toggleCheckbox={props.toggleCheckbox}
+        getItemName={props.getItemName}
+        getItemIcon={props.getItemIcon}
+        handleScroll={props.handleScroll}
+        title={props.title}
+        showTitles={props.showTitles}
+        getItemId={props.getItemId}
+        i18n={props.i18n}
+      />
+      {selected > 0 && <FooterActions selected={selected} {...props} />}
     </div>
   )
 }
 
-// <div class="uppy-Dashboard-actions">
-//  <button class="uppy-u-reset uppy-c-btn uppy-c-btn-primary uppy-Dashboard-actionsBtn" type="button">Select</button>
-// </div>
+module.exports = Browser

+ 14 - 0
src/views/ProviderView/FooterActions.js

@@ -0,0 +1,14 @@
+const { h } = require('preact')
+
+module.exports = (props) => {
+  return <div class="uppy-ProviderBrowser-footer">
+    <button class="uppy-u-reset uppy-c-btn uppy-c-btn-primary" onclick={props.done}>
+      {props.i18n('selectXFiles', {
+        smart_count: props.selected
+      })}
+    </button>
+    <button class="uppy-u-reset uppy-c-btn uppy-c-btn-link" onclick={props.cancel}>
+      {props.i18n('cancel')}
+    </button>
+  </div>
+}

+ 4 - 4
src/views/ProviderView/Item.js

@@ -12,9 +12,9 @@ module.exports = (props) => {
     ev.preventDefault()
     // when file is clicked, select it, but when folder is clicked, open it
     if (props.type === 'folder') {
-      return props.handleClick(ev)
+      return props.handleFolderClick(ev)
     }
-    props.handleCheckboxClick(ev)
+    props.handleClick(ev)
   }
 
   return (
@@ -27,13 +27,13 @@ module.exports = (props) => {
           id={props.id}
           checked={props.isChecked}
           disabled={props.isDisabled}
-          onchange={props.handleCheckboxClick}
+          onchange={props.handleClick}
           onkeyup={stop}
           onkeydown={stop}
           onkeypress={stop} />
         <label
           for={props.id}
-          onclick={props.handleCheckboxClick}
+          onclick={props.handleClick}
          />
       </div>
       <button type="button"

+ 3 - 4
src/views/ProviderView/ItemList.js

@@ -24,10 +24,10 @@ module.exports = (props) => {
             type: 'folder',
             // active: props.activeRow(folder),
             getItemIcon: () => props.getItemIcon(folder),
-            handleClick: () => props.handleFolderClick(folder),
             isDisabled: isDisabled,
             isChecked: isChecked,
-            handleCheckboxClick: (e) => props.toggleCheckbox(e, folder),
+            handleFolderClick: () => props.handleFolderClick(folder),
+            handleClick: (e) => props.toggleCheckbox(e, folder),
             columns: props.columns,
             showTitles: props.showTitles
           })
@@ -39,10 +39,9 @@ module.exports = (props) => {
             type: 'file',
             // active: props.activeRow(file),
             getItemIcon: () => props.getItemIcon(file),
-            handleClick: () => props.handleFileClick(file),
             isDisabled: false,
             isChecked: props.isChecked(file),
-            handleCheckboxClick: (e) => props.toggleCheckbox(e, file),
+            handleClick: (e) => props.toggleCheckbox(e, file),
             columns: props.columns,
             showTitles: props.showTitles
           })

+ 112 - 114
src/views/ProviderView/index.js

@@ -2,7 +2,27 @@ const AuthView = require('./AuthView')
 const Browser = require('./Browser')
 const LoaderView = require('./Loader')
 const Utils = require('../../core/Utils')
-const { h } = require('preact')
+const { h, Component } = require('preact')
+
+/**
+ * Array.prototype.findIndex ponyfill for old browsers.
+ */
+function findIndex (array, predicate) {
+  for (let i = 0; i < array.length; i++) {
+    if (predicate(array[i])) return i
+  }
+  return -1
+}
+
+class CloseWrapper extends Component {
+  componentWillUnmount () {
+    this.props.onUnmount()
+  }
+
+  render () {
+    return this.props.children[0]
+  }
+}
 
 /**
  * Class to easily generate generic views for plugins
@@ -54,7 +74,6 @@ module.exports = class ProviderView {
     this.opts = Object.assign({}, defaultOptions, opts)
 
     // Logic
-    this.updateFolderState = this.updateFolderState.bind(this)
     this.addFile = this.addFile.bind(this)
     this.filterItems = this.filterItems.bind(this)
     this.filterQuery = this.filterQuery.bind(this)
@@ -73,15 +92,17 @@ module.exports = class ProviderView {
     this.handleError = this.handleError.bind(this)
     this.handleScroll = this.handleScroll.bind(this)
     this.donePicking = this.donePicking.bind(this)
-
-    this.plugin.uppy.on('file-removed', this.updateFolderState)
+    this.cancelPicking = this.cancelPicking.bind(this)
+    this.clearSelection = this.clearSelection.bind(this)
 
     // Visual
     this.render = this.render.bind(this)
+
+    this.clearSelection()
   }
 
   tearDown () {
-    this.plugin.uppy.off('file-removed', this.updateFolderState)
+    // Nothing.
   }
 
   _updateFilesAndFolders (res, files, folders) {
@@ -123,7 +144,7 @@ module.exports = class ProviderView {
         let updatedDirectories
 
         const state = this.plugin.getPluginState()
-        const index = state.directories.findIndex((dir) => id === dir.id)
+        const index = findIndex(state.directories, (dir) => id === dir.id)
 
         if (index !== -1) {
           updatedDirectories = state.directories.slice(0, index + 1)
@@ -149,8 +170,9 @@ module.exports = class ProviderView {
     this.lastCheckbox = undefined
   }
 
-  addFile (file, isCheckbox = false) {
+  addFile (file) {
     const tagFile = {
+      id: this.providerFileToId(file),
       source: this.plugin.id,
       data: this.plugin.getItemData(file),
       name: this.plugin.getItemName(file) || this.plugin.getItemId(file),
@@ -179,9 +201,13 @@ module.exports = class ProviderView {
     } catch (err) {
       // Nothing, restriction errors handled in Core
     }
-    if (!isCheckbox) {
-      this.donePicking()
-    }
+  }
+
+  removeFile (id) {
+    const { currentSelection } = this.plugin.getPluginState()
+    this.plugin.setPluginState({
+      currentSelection: currentSelection.filter((file) => file.id !== id)
+    })
   }
 
   /**
@@ -220,6 +246,9 @@ module.exports = class ProviderView {
 
   filterItems (items) {
     const state = this.plugin.getPluginState()
+    if (state.filterInput === '') {
+      return items
+    }
     return items.filter((folder) => {
       return this.plugin.getItemName(folder).toLowerCase().indexOf(state.filterInput.toLowerCase()) !== -1
     })
@@ -311,17 +340,9 @@ module.exports = class ProviderView {
     return this.plugin.getPluginState().activeRow === this.plugin.getItemId(file)
   }
 
-  isChecked (item) {
-    const itemId = this.providerFileToId(item)
-    if (this.plugin.isFolder(item)) {
-      const state = this.plugin.getPluginState()
-      const folders = state.selectedFolders || {}
-      if (itemId in folders) {
-        return folders[itemId]
-      }
-      return false
-    }
-    return (itemId in this.plugin.uppy.getState().files)
+  isChecked (file) {
+    const { currentSelection } = this.plugin.getPluginState()
+    return currentSelection.some((item) => item === file)
   }
 
   /**
@@ -339,11 +360,11 @@ module.exports = class ProviderView {
     }
     folders[folderId] = {loading: true, files: []}
     this.plugin.setPluginState({selectedFolders: folders})
-    this.Provider.list(this.plugin.getItemRequestPath(folder)).then((res) => {
+    return this.Provider.list(this.plugin.getItemRequestPath(folder)).then((res) => {
       let files = []
       this.plugin.getItemSubList(res).forEach((item) => {
         if (!this.plugin.isFolder(item)) {
-          this.addFile(item, true)
+          this.addFile(item)
           files.push(this.providerFileToId(item))
         }
       })
@@ -368,102 +389,44 @@ module.exports = class ProviderView {
     })
   }
 
-  removeFolder (folderId) {
-    let state = this.plugin.getPluginState()
-    let folders = state.selectedFolders || {}
-    if (!(folderId in folders)) {
-      return
-    }
-    let folder = folders[folderId]
-    if (folder.loading) {
-      return
-    }
-    // deepcopy the files before iteration because the
-    // original array constantly gets mutated during
-    // the iteration by updateFolderState as each file
-    // is removed and 'core:file-removed' is emitted.
-    const files = folder.files.concat([])
-    for (const fileId of files) {
-      if (fileId in this.plugin.uppy.getState().files) {
-        this.plugin.uppy.removeFile(fileId)
-      }
-    }
-    delete folders[folderId]
-    this.plugin.setPluginState({selectedFolders: folders})
-  }
-
-  /**
-   * Updates selected folders state everytime file is being removed.
-   *
-   * Note that this is only important when files are getting removed from the
-   * main screen, and will do nothing when you uncheck folder directly, since
-   * it's already been done in removeFolder method.
-   */
-  updateFolderState (file) {
-    let state = this.plugin.getPluginState()
-    let folders = state.selectedFolders || {}
-    for (let folderId in folders) {
-      let folder = folders[folderId]
-      if (folder.loading) {
-        continue
-      }
-      let i = folder.files.indexOf(file.id)
-      if (i > -1) {
-        folder.files.splice(i, 1)
-      }
-      if (!folder.files.length) {
-        delete folders[folderId]
-      }
-    }
-    this.plugin.setPluginState({selectedFolders: folders})
-  }
-
   /**
    * Toggles file/folder checkbox to on/off state while updating files list.
    *
    * Note that some extra complexity comes from supporting shift+click to
    * toggle multiple checkboxes at once, which is done by getting all files
-   * in between last checked file and current one, and applying an on/off state
-   * for all of them, depending on current file state.
+   * in between last checked file and current one.
    */
   toggleCheckbox (e, file) {
     e.stopPropagation()
     e.preventDefault()
-    let { folders, files, filterInput } = this.plugin.getPluginState()
-    let items = folders.concat(files)
-    if (filterInput !== '') {
-      items = this.filterItems(items)
-    }
-    let itemsToToggle = [file]
+    let { folders, files } = this.plugin.getPluginState()
+    let items = this.filterItems(folders.concat(files))
+
+    // Shift-clicking selects a single consecutive list of items
+    // starting at the previous click and deselects everything else.
     if (this.lastCheckbox && e.shiftKey) {
-      let prevIndex = items.indexOf(this.lastCheckbox)
-      let currentIndex = items.indexOf(file)
+      let currentSelection
+      const prevIndex = items.indexOf(this.lastCheckbox)
+      const currentIndex = items.indexOf(file)
       if (prevIndex < currentIndex) {
-        itemsToToggle = items.slice(prevIndex, currentIndex + 1)
+        currentSelection = items.slice(prevIndex, currentIndex + 1)
       } else {
-        itemsToToggle = items.slice(currentIndex, prevIndex + 1)
+        currentSelection = items.slice(currentIndex, prevIndex + 1)
       }
+      this.plugin.setPluginState({ currentSelection })
+      return
     }
+
     this.lastCheckbox = file
+    const { currentSelection } = this.plugin.getPluginState()
     if (this.isChecked(file)) {
-      for (let item of itemsToToggle) {
-        const itemId = this.providerFileToId(item)
-        if (this.plugin.isFolder(item)) {
-          this.removeFolder(itemId)
-        } else {
-          if (itemId in this.plugin.uppy.getState().files) {
-            this.plugin.uppy.removeFile(itemId)
-          }
-        }
-      }
+      this.plugin.setPluginState({
+        currentSelection: currentSelection.filter((item) => item !== file)
+      })
     } else {
-      for (let item of itemsToToggle) {
-        if (this.plugin.isFolder(item)) {
-          this.addFolder(item)
-        } else {
-          this.addFile(item, true)
-        }
-      }
+      this.plugin.setPluginState({
+        currentSelection: currentSelection.concat([file])
+      })
     }
   }
 
@@ -538,10 +501,34 @@ module.exports = class ProviderView {
   }
 
   donePicking () {
+    const { currentSelection } = this.plugin.getPluginState()
+    const promises = currentSelection.map((file) => {
+      if (this.plugin.isFolder(file)) {
+        return this.addFolder(file)
+      } else {
+        return this.addFile(file)
+      }
+    })
+
+    this._loaderWrapper(Promise.all(promises), () => {
+      this.clearSelection()
+
+      const dashboard = this.plugin.uppy.getPlugin('Dashboard')
+      if (dashboard) dashboard.hideAllPanels()
+    }, () => {})
+  }
+
+  cancelPicking () {
+    this.clearSelection()
+
     const dashboard = this.plugin.uppy.getPlugin('Dashboard')
     if (dashboard) dashboard.hideAllPanels()
   }
 
+  clearSelection () {
+    this.plugin.setPluginState({ currentSelection: [] })
+  }
+
   // displays loader view while asynchronous request is being made.
   _loaderWrapper (promise, then, catch_) {
     promise
@@ -554,26 +541,32 @@ module.exports = class ProviderView {
     const { authenticated, checkAuthInProgress, loading } = this.plugin.getPluginState()
 
     if (loading) {
-      return LoaderView()
+      return (
+        <CloseWrapper onUnmount={this.clearSelection}>
+          <LoaderView />
+        </CloseWrapper>
+      )
     }
 
     if (!authenticated) {
-      return h(AuthView, {
-        pluginName: this.plugin.title,
-        pluginIcon: this.plugin.icon,
-        demo: this.plugin.opts.demo,
-        checkAuth: this.checkAuth,
-        handleAuth: this.handleAuth,
-        handleDemoAuth: this.handleDemoAuth,
-        checkAuthInProgress: checkAuthInProgress
-      })
+      return (
+        <CloseWrapper onUnmount={this.clearSelection}>
+          <AuthView
+            pluginName={this.plugin.title}
+            pluginIcon={this.plugin.icon}
+            demo={this.plugin.opts.demo}
+            checkAuth={this.checkAuth}
+            handleAuth={this.handleAuth}
+            handleDemoAuth={this.handleDemoAuth}
+            checkAuthInProgress={checkAuthInProgress} />
+        </CloseWrapper>
+      )
     }
 
     const browserProps = Object.assign({}, this.plugin.getPluginState(), {
       username: this.username,
       getNextFolder: this.getNextFolder,
       getFolder: this.getFolder,
-      addFile: this.addFile,
       filterItems: this.filterItems,
       filterQuery: this.filterQuery,
       toggleSearch: this.toggleSearch,
@@ -589,6 +582,7 @@ module.exports = class ProviderView {
       getItemIcon: this.plugin.getItemIcon,
       handleScroll: this.handleScroll,
       done: this.donePicking,
+      cancel: this.cancelPicking,
       title: this.plugin.title,
       viewType: this.opts.viewType,
       showTitles: this.opts.showTitles,
@@ -598,6 +592,10 @@ module.exports = class ProviderView {
       i18n: this.plugin.uppy.i18n
     })
 
-    return Browser(browserProps)
+    return (
+      <CloseWrapper onUnmount={this.clearSelection}>
+        <Browser {...browserProps} />
+      </CloseWrapper>
+    )
   }
 }