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

Wait for user to press Done in provider view before adding files.
This commit is contained in:
Artur Paikin 2018-05-31 18:58:13 -04:00 committed by GitHub
commit d8fea4bb40
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 187 additions and 181 deletions

View file

@ -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, dont 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, dont 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)

View file

@ -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'
}
}

View file

@ -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%;
}

View file

@ -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

View file

@ -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>
}

View file

@ -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"

View file

@ -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
})

View file

@ -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>
)
}
}