@uppy/provider-views
Advanced tools
Comparing version 3.1.0 to 3.2.0
# @uppy/provider-views | ||
## 3.2.0 | ||
Released: 2023-04-04 | ||
Included in: Uppy v3.7.0 | ||
- @uppy/provider-views: fix race condition when adding folders (Mikael Finstad / #4384) | ||
- @uppy/provider-views: UI: Use form attribite with a form in doc root to prevent outer form submit (Artur Paikin / #4283) | ||
## 3.0.2 | ||
@@ -4,0 +12,0 @@ |
import { h } from 'preact'; | ||
import classNames from 'classnames'; | ||
import remoteFileObjToLocal from '@uppy/utils/lib/remoteFileObjToLocal'; | ||
import Filter from "./Filter.js"; | ||
import SearchFilterInput from "./SearchFilterInput.js"; | ||
import FooterActions from "./FooterActions.js"; | ||
@@ -25,9 +25,15 @@ import Item from "./Item/index.js"; | ||
validateRestrictions, | ||
showFilter, | ||
filterQuery, | ||
filterInput, | ||
isLoading, | ||
showSearchFilter, | ||
search, | ||
searchTerm, | ||
clearSearch, | ||
searchOnInput, | ||
searchInputLabel, | ||
clearSearchLabel, | ||
getNextFolder, | ||
cancel, | ||
done, | ||
columns | ||
columns, | ||
noResultsLabel | ||
} = props; | ||
@@ -37,15 +43,27 @@ const selected = currentSelection.length; | ||
className: classNames('uppy-ProviderBrowser', `uppy-ProviderBrowser-viewType--${viewType}`) | ||
}, h("div", { | ||
}, headerComponent && h("div", { | ||
className: "uppy-ProviderBrowser-header" | ||
}, h("div", { | ||
className: classNames('uppy-ProviderBrowser-headerBar', !showBreadcrumbs && 'uppy-ProviderBrowser-headerBar--simple') | ||
}, headerComponent)), showFilter && h(Filter, { | ||
i18n: i18n, | ||
filterQuery: filterQuery, | ||
filterInput: filterInput | ||
}), (() => { | ||
}, headerComponent)), showSearchFilter && h("div", { | ||
class: "uppy-ProviderBrowser-searchFilter" | ||
}, h(SearchFilterInput, { | ||
search: search, | ||
searchTerm: searchTerm, | ||
clearSearch: clearSearch, | ||
inputLabel: searchInputLabel, | ||
clearSearchLabel: clearSearchLabel, | ||
inputClassName: "uppy-ProviderBrowser-searchFilterInput", | ||
searchOnInput: searchOnInput | ||
})), (() => { | ||
if (isLoading) { | ||
return h("div", { | ||
className: "uppy-Provider-loading" | ||
}, h("span", null, i18n('loading'))); | ||
} | ||
if (!folders.length && !files.length) { | ||
return h("div", { | ||
className: "uppy-Provider-empty" | ||
}, i18n('noFilesFound')); | ||
}, noResultsLabel); | ||
} | ||
@@ -52,0 +70,0 @@ |
import { h } from 'preact'; | ||
import classNames from 'classnames'; | ||
@@ -17,2 +18,5 @@ function GridListItem(props) { | ||
} = props; | ||
const checkBoxClassName = classNames('uppy-u-reset', 'uppy-ProviderBrowserItem-checkbox', 'uppy-ProviderBrowserItem-checkbox--grid', { | ||
'uppy-ProviderBrowserItem-checkbox--is-checked': isChecked | ||
}); | ||
return h("li", { | ||
@@ -23,3 +27,3 @@ className: className, | ||
type: "checkbox", | ||
className: `uppy-u-reset uppy-ProviderBrowserItem-checkbox ${isChecked ? 'uppy-ProviderBrowserItem-checkbox--is-checked' : ''} uppy-ProviderBrowserItem-checkbox--grid`, | ||
className: checkBoxClassName, | ||
onChange: toggleCheckbox, | ||
@@ -36,7 +40,5 @@ onKeyDown: recordShiftKeyPress, | ||
className: "uppy-u-reset uppy-ProviderBrowserItem-inner" | ||
}, h("span", { | ||
className: "uppy-ProviderBrowserItem-inner-relative" | ||
}, itemIconEl, showTitles && title, children))); | ||
}, itemIconEl, showTitles && title, children)); | ||
} | ||
export default GridListItem; |
@@ -53,3 +53,4 @@ function _extends() { _extends = Object.assign ? Object.assign.bind() : function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); } | ||
rel: "noopener noreferrer", | ||
className: "uppy-ProviderBrowserItem-author" | ||
className: "uppy-ProviderBrowserItem-author", | ||
tabIndex: "-1" | ||
}, author.name)) | ||
@@ -56,0 +57,0 @@ ); |
@@ -8,2 +8,3 @@ function _classPrivateFieldLooseBase(receiver, privateKey) { if (!Object.prototype.hasOwnProperty.call(receiver, privateKey)) { throw new TypeError("attempted to use private field on non-instance"); } return receiver; } | ||
import { h } from 'preact'; | ||
import { getSafeFileId } from '@uppy/utils/lib/generateFileID'; | ||
import AuthView from "./AuthView.js"; | ||
@@ -16,3 +17,3 @@ import Header from "./Header.js"; | ||
const packageJson = { | ||
"version": "3.1.0" | ||
"version": "3.2.0" | ||
}; | ||
@@ -71,2 +72,3 @@ | ||
this.filterQuery = this.filterQuery.bind(this); | ||
this.clearFilter = this.clearFilter.bind(this); | ||
this.getFolder = this.getFolder.bind(this); | ||
@@ -77,3 +79,2 @@ this.getNextFolder = this.getNextFolder.bind(this); | ||
this.handleScroll = this.handleScroll.bind(this); | ||
this.listAllFiles = this.listAllFiles.bind(this); | ||
this.donePicking = this.donePicking.bind(this); // Visual | ||
@@ -104,4 +105,7 @@ | ||
*/ | ||
getFolder(id, name) { | ||
return this.sharedHandler.loaderWrapper(this.provider.list(id), res => { | ||
async getFolder(id, name) { | ||
this.setLoading(true); | ||
try { | ||
const res = await this.provider.list(id); | ||
const folders = []; | ||
@@ -130,3 +134,7 @@ const files = []; | ||
}); | ||
}, this.handleError); | ||
} catch (err) { | ||
this.handleError(err); | ||
} finally { | ||
this.setLoading(false); | ||
} | ||
} | ||
@@ -172,85 +180,11 @@ /** | ||
filterQuery(e) { | ||
const state = this.plugin.getPluginState(); | ||
this.plugin.setPluginState({ ...state, | ||
filterInput: e ? e.target.value : '' | ||
filterQuery(input) { | ||
this.plugin.setPluginState({ | ||
filterInput: input | ||
}); | ||
} | ||
/** | ||
* Adds all files found inside of specified folder. | ||
* | ||
* Uses separated state while folder contents are being fetched and | ||
* mantains list of selected folders, which are separated from files. | ||
*/ | ||
addFolder(folder) { | ||
const folderId = this.providerFileToId(folder); | ||
const folders = { ...this.plugin.getPluginState().selectedFolders | ||
}; | ||
if (folderId in folders && folders[folderId].loading) { | ||
return; | ||
} | ||
folders[folderId] = { | ||
loading: true, | ||
files: [] | ||
}; | ||
clearFilter() { | ||
this.plugin.setPluginState({ | ||
selectedFolders: { ...folders | ||
} | ||
}); // eslint-disable-next-line consistent-return | ||
return this.listAllFiles(folder.requestPath).then(files => { | ||
let count = 0; // If the same folder is added again, we don't want to send | ||
// X amount of duplicate file notifications, we want to say | ||
// the folder was already added. This checks if all files are duplicate, | ||
// if that's the case, we don't add the files. | ||
files.forEach(file => { | ||
const id = this.providerFileToId(file); | ||
if (!this.plugin.uppy.checkIfFileAlreadyExists(id)) { | ||
count++; | ||
} | ||
}); | ||
if (count > 0) { | ||
files.forEach(file => this.addFile(file)); | ||
} | ||
const ids = files.map(this.providerFileToId); | ||
folders[folderId] = { | ||
loading: false, | ||
files: ids | ||
}; | ||
this.plugin.setPluginState({ | ||
selectedFolders: folders, | ||
filterInput: '' | ||
}); | ||
let message; | ||
if (count === 0) { | ||
message = this.plugin.uppy.i18n('folderAlreadyAdded', { | ||
folder: folder.name | ||
}); | ||
} else if (files.length) { | ||
message = this.plugin.uppy.i18n('folderAdded', { | ||
smart_count: count, | ||
folder: folder.name | ||
}); | ||
} else { | ||
message = this.plugin.uppy.i18n('emptyFolderAdded'); | ||
} | ||
this.plugin.uppy.info(message); | ||
}).catch(e => { | ||
const selectedFolders = { ...this.plugin.getPluginState().selectedFolders | ||
}; | ||
delete selectedFolders[folderId]; | ||
this.plugin.setPluginState({ | ||
selectedFolders | ||
}); | ||
this.handleError(e); | ||
filterInput: '' | ||
}); | ||
@@ -333,40 +267,93 @@ } | ||
async listAllFiles(path, files) { | ||
if (files === void 0) { | ||
files = null; | ||
} | ||
async *recursivelyListAllFiles(path) { | ||
let curPath = path; // need to repeat the list call until there are no more pages | ||
files = files || []; // eslint-disable-line no-param-reassign | ||
while (curPath) { | ||
const res = await this.provider.list(curPath); | ||
const res = await this.provider.list(path); | ||
res.items.forEach(item => { | ||
if (!item.isFolder) { | ||
files.push(item); | ||
} else { | ||
this.addFolder(item); | ||
for (const item of res.items) { | ||
if (item.isFolder) { | ||
// recursively call self for folder | ||
yield* this.recursivelyListAllFiles(item.requestPath); | ||
} else { | ||
yield item; | ||
} | ||
} | ||
}); | ||
const moreFiles = res.nextPagePath; | ||
if (moreFiles) { | ||
return this.listAllFiles(moreFiles, files); | ||
curPath = res.nextPagePath; | ||
} | ||
return files; | ||
} | ||
donePicking() { | ||
const { | ||
currentSelection | ||
} = this.plugin.getPluginState(); | ||
const promises = currentSelection.map(file => { | ||
if (file.isFolder) { | ||
return this.addFolder(file); | ||
} | ||
async donePicking() { | ||
this.setLoading(true); | ||
return this.addFile(file); | ||
}); | ||
this.sharedHandler.loaderWrapper(Promise.all(promises), () => { | ||
try { | ||
const { | ||
currentSelection | ||
} = this.plugin.getPluginState(); | ||
const messages = []; | ||
const newFiles = []; // eslint-disable-next-line no-unreachable-loop | ||
for (const file of currentSelection) { | ||
if (file.isFolder) { | ||
const { | ||
requestPath, | ||
name | ||
} = file; | ||
let isEmpty = true; | ||
let numNewFiles = 0; | ||
for await (const fileInFolder of this.recursivelyListAllFiles(requestPath)) { | ||
const tagFile = this.getTagFile(fileInFolder); | ||
const id = getSafeFileId(tagFile); // If the same folder is added again, we don't want to send | ||
// X amount of duplicate file notifications, we want to say | ||
// the folder was already added. This checks if all files are duplicate, | ||
// if that's the case, we don't add the files. | ||
if (!this.plugin.uppy.checkIfFileAlreadyExists(id)) { | ||
newFiles.push(fileInFolder); | ||
numNewFiles++; | ||
} | ||
isEmpty = false; | ||
} | ||
let message; | ||
if (isEmpty) { | ||
message = this.plugin.uppy.i18n('emptyFolderAdded'); | ||
} else if (numNewFiles === 0) { | ||
message = this.plugin.uppy.i18n('folderAlreadyAdded', { | ||
folder: name | ||
}); | ||
} else { | ||
message = this.plugin.uppy.i18n('folderAdded', { | ||
smart_count: numNewFiles, | ||
folder: name | ||
}); | ||
} | ||
messages.push(message); | ||
} else { | ||
newFiles.push(file); | ||
} | ||
} // Note: this.plugin.uppy.addFiles must be only run once we are done fetching all files, | ||
// because it will cause the loading screen to disappear, | ||
// and that will allow the user to start the upload, so we need to make sure we have | ||
// finished all async operations before we add any file | ||
// see https://github.com/transloadit/uppy/pull/4384 | ||
this.plugin.uppy.log('Adding remote provider files'); | ||
this.plugin.uppy.addFiles(newFiles.map(file => this.getTagFile(file))); | ||
this.plugin.setPluginState({ | ||
filterInput: '' | ||
}); | ||
messages.forEach(message => this.plugin.uppy.info(message)); | ||
this.clearSelection(); | ||
}, () => {}); | ||
} catch (err) { | ||
this.handleError(err); | ||
} finally { | ||
this.setLoading(false); | ||
} | ||
} | ||
@@ -385,2 +372,5 @@ | ||
} = this.plugin.getPluginState(); | ||
const { | ||
i18n | ||
} = this.plugin.uppy; | ||
@@ -406,3 +396,3 @@ if (!didFirstRender) { | ||
filterItems | ||
} = this.sharedHandler; | ||
} = this; | ||
const hasInput = filterInput !== ''; | ||
@@ -417,3 +407,3 @@ const headerProps = { | ||
username: this.username, | ||
i18n: this.plugin.uppy.i18n | ||
i18n | ||
}; | ||
@@ -430,7 +420,13 @@ const browserProps = { | ||
getFolder: this.getFolder, | ||
filterItems: this.sharedHandler.filterItems, | ||
filterQuery: this.filterQuery, | ||
// For SearchFilterInput component | ||
showSearchFilter: targetViewOptions.showFilter, | ||
search: this.filterQuery, | ||
clearSearch: this.clearFilter, | ||
searchTerm: filterInput, | ||
searchOnInput: true, | ||
searchInputLabel: i18n('filter'), | ||
clearSearchLabel: i18n('resetFilter'), | ||
noResultsLabel: i18n('noFilesFound'), | ||
logout: this.logout, | ||
handleScroll: this.handleScroll, | ||
listAllFiles: this.listAllFiles, | ||
done: this.donePicking, | ||
@@ -442,3 +438,2 @@ cancel: this.cancelPicking, | ||
showTitles: targetViewOptions.showTitles, | ||
showFilter: targetViewOptions.showFilter, | ||
showBreadcrumbs: targetViewOptions.showBreadcrumbs, | ||
@@ -445,0 +440,0 @@ pluginIcon: this.plugin.icon, |
@@ -8,13 +8,12 @@ function _classPrivateFieldLooseBase(receiver, privateKey) { if (!Object.prototype.hasOwnProperty.call(receiver, privateKey)) { throw new TypeError("attempted to use private field on non-instance"); } return receiver; } | ||
import { h } from 'preact'; | ||
import SearchInput from "./InputView.js"; | ||
import SearchFilterInput from "../SearchFilterInput.js"; | ||
import Browser from "../Browser.js"; | ||
import LoaderView from "../Loader.js"; | ||
import Header from "./Header.js"; | ||
import CloseWrapper from '../CloseWrapper.js'; | ||
import View from '../View.js'; | ||
const packageJson = { | ||
"version": "3.1.0" | ||
"version": "3.2.0" | ||
}; | ||
/** | ||
* Class to easily generate generic views for Provider plugins | ||
* SearchProviderView, used for Unsplash and future image search providers. | ||
* Extends generic View, shared with regular providers like Google Drive and Instagram. | ||
*/ | ||
@@ -47,10 +46,9 @@ | ||
this.search = this.search.bind(this); | ||
this.triggerSearchInput = this.triggerSearchInput.bind(this); | ||
this.addFile = this.addFile.bind(this); | ||
this.clearSearch = this.clearSearch.bind(this); | ||
this.resetPluginState = this.resetPluginState.bind(this); | ||
this.handleScroll = this.handleScroll.bind(this); | ||
this.donePicking = this.donePicking.bind(this); // Visual | ||
this.render = this.render.bind(this); // Set default state for the plugin | ||
this.plugin.setPluginState({ | ||
this.render = this.render.bind(this); | ||
this.defaultState = { | ||
isInputMode: true, | ||
@@ -63,3 +61,5 @@ files: [], | ||
searchTerm: null | ||
}); | ||
}; // Set default state for the plugin | ||
this.plugin.setPluginState(this.defaultState); | ||
} // eslint-disable-next-line class-methods-use-this | ||
@@ -71,12 +71,7 @@ | ||
clearSelection() { | ||
this.plugin.setPluginState({ | ||
currentSelection: [], | ||
isInputMode: true, | ||
files: [], | ||
searchTerm: null | ||
}); | ||
resetPluginState() { | ||
this.plugin.setPluginState(this.defaultState); | ||
} | ||
search(query) { | ||
async search(query) { | ||
const { | ||
@@ -88,13 +83,23 @@ searchTerm | ||
// no need to search again as this is the same as the previous search | ||
return undefined; | ||
return; | ||
} | ||
return this.sharedHandler.loaderWrapper(this.provider.search(query), res => { | ||
this.setLoading(true); | ||
try { | ||
const res = await this.provider.search(query); | ||
_classPrivateFieldLooseBase(this, _updateFilesAndInputMode)[_updateFilesAndInputMode](res, []); | ||
}, this.handleError); | ||
} catch (err) { | ||
this.handleError(err); | ||
} finally { | ||
this.setLoading(false); | ||
} | ||
} | ||
triggerSearchInput() { | ||
clearSearch() { | ||
this.plugin.setPluginState({ | ||
isInputMode: true | ||
currentSelection: [], | ||
files: [], | ||
searchTerm: null | ||
}); | ||
@@ -129,6 +134,5 @@ } | ||
} = this.plugin.getPluginState(); | ||
const promises = currentSelection.map(file => this.addFile(file)); | ||
this.sharedHandler.loaderWrapper(Promise.all(promises), () => { | ||
this.clearSelection(); | ||
}, () => {}); | ||
this.plugin.uppy.log('Adding remote search provider files'); | ||
this.plugin.uppy.addFiles(currentSelection.map(file => this.getTagFile(file))); | ||
this.resetPluginState(); | ||
} | ||
@@ -148,2 +152,5 @@ | ||
} = this.plugin.getPluginState(); | ||
const { | ||
i18n | ||
} = this.plugin.uppy; | ||
@@ -168,3 +175,3 @@ if (!didFirstRender) { | ||
filterItems | ||
} = this.sharedHandler; | ||
} = this; | ||
const hasInput = filterInput !== ''; | ||
@@ -180,7 +187,11 @@ const browserProps = { | ||
cancel: this.cancelPicking, | ||
headerComponent: Header({ | ||
search: this.search, | ||
i18n: this.plugin.uppy.i18n, | ||
searchTerm | ||
}), | ||
// For SearchFilterInput component | ||
showSearchFilter: targetViewOptions.showFilter, | ||
search: this.search, | ||
clearSearch: this.clearSearch, | ||
searchTerm, | ||
searchOnInput: false, | ||
searchInputLabel: i18n('search'), | ||
clearSearchLabel: i18n('resetSearch'), | ||
noResultsLabel: i18n('noSearchResults'), | ||
title: this.plugin.title, | ||
@@ -190,5 +201,6 @@ viewType: targetViewOptions.viewType, | ||
showFilter: targetViewOptions.showFilter, | ||
isLoading: loading, | ||
showBreadcrumbs: targetViewOptions.showBreadcrumbs, | ||
pluginIcon: this.plugin.icon, | ||
i18n: this.plugin.uppy.i18n, | ||
i18n, | ||
uppyFiles: this.plugin.uppy.getFiles(), | ||
@@ -200,21 +212,20 @@ validateRestrictions: function () { | ||
if (loading) { | ||
return h(CloseWrapper, { | ||
onUnmount: this.clearSelection | ||
}, h(LoaderView, { | ||
i18n: this.plugin.uppy.i18n | ||
})); | ||
} | ||
if (isInputMode) { | ||
return h(CloseWrapper, { | ||
onUnmount: this.clearSelection | ||
}, h(SearchInput, { | ||
onUnmount: this.resetPluginState | ||
}, h("div", { | ||
className: "uppy-SearchProvider" | ||
}, h(SearchFilterInput, { | ||
search: this.search, | ||
i18n: this.plugin.uppy.i18n | ||
})); | ||
clearSelection: this.clearSelection, | ||
inputLabel: i18n('enterTextToSearch'), | ||
buttonLabel: i18n('searchImages'), | ||
inputClassName: "uppy-c-textInput uppy-SearchProvider-input", | ||
buttonCSSClassName: "uppy-SearchProvider-searchButton", | ||
showButton: true | ||
}))); | ||
} | ||
return h(CloseWrapper, { | ||
onUnmount: this.clearSelection | ||
onUnmount: this.resetPluginState | ||
}, h(Browser, browserProps)); | ||
@@ -231,2 +242,3 @@ } | ||
this.plugin.setPluginState({ | ||
currentSelection: [], | ||
isInputMode: false, | ||
@@ -233,0 +245,0 @@ files, |
123
lib/View.js
import getFileType from '@uppy/utils/lib/getFileType'; | ||
import isPreviewSupported from '@uppy/utils/lib/isPreviewSupported'; | ||
import generateFileID from '@uppy/utils/lib/generateFileID'; // TODO: now that we have a shared `View` class, | ||
// `SharedHandler` could be cleaned up and moved into here | ||
import SharedHandler from './SharedHandler.js'; | ||
import remoteFileObjToLocal from '@uppy/utils/lib/remoteFileObjToLocal'; | ||
export default class View { | ||
constructor(plugin, opts) { | ||
this.filterItems = items => { | ||
const state = this.plugin.getPluginState(); | ||
if (!state.filterInput || state.filterInput === '') { | ||
return items; | ||
} | ||
return items.filter(folder => { | ||
return folder.name.toLowerCase().indexOf(state.filterInput.toLowerCase()) !== -1; | ||
}); | ||
}; | ||
this.recordShiftKeyPress = e => { | ||
this.isShiftKeyPressed = e.shiftKey; | ||
}; | ||
this.toggleCheckbox = (e, file) => { | ||
e.stopPropagation(); | ||
e.preventDefault(); | ||
e.currentTarget.focus(); | ||
const { | ||
folders, | ||
files | ||
} = this.plugin.getPluginState(); | ||
const 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 && this.isShiftKeyPressed) { | ||
const prevIndex = items.indexOf(this.lastCheckbox); | ||
const currentIndex = items.indexOf(file); | ||
const currentSelection = prevIndex < currentIndex ? items.slice(prevIndex, currentIndex + 1) : items.slice(currentIndex, prevIndex + 1); | ||
const reducedCurrentSelection = []; // Check restrictions on each file in currentSelection, | ||
// reduce it to only contain files that pass restrictions | ||
for (const item of currentSelection) { | ||
const { | ||
uppy | ||
} = this.plugin; | ||
const restrictionError = uppy.validateRestrictions(remoteFileObjToLocal(item), [...uppy.getFiles(), ...reducedCurrentSelection]); | ||
if (!restrictionError) { | ||
reducedCurrentSelection.push(item); | ||
} else { | ||
uppy.info({ | ||
message: restrictionError.message | ||
}, 'error', uppy.opts.infoTimeout); | ||
} | ||
} | ||
this.plugin.setPluginState({ | ||
currentSelection: reducedCurrentSelection | ||
}); | ||
return; | ||
} | ||
this.lastCheckbox = file; | ||
const { | ||
currentSelection | ||
} = this.plugin.getPluginState(); | ||
if (this.isChecked(file)) { | ||
this.plugin.setPluginState({ | ||
currentSelection: currentSelection.filter(item => item.id !== file.id) | ||
}); | ||
} else { | ||
this.plugin.setPluginState({ | ||
currentSelection: currentSelection.concat([file]) | ||
}); | ||
} | ||
}; | ||
this.isChecked = file => { | ||
const { | ||
currentSelection | ||
} = this.plugin.getPluginState(); // comparing id instead of the file object, because the reference to the object | ||
// changes when we switch folders, and the file list is updated | ||
return currentSelection.some(item => item.id === file.id); | ||
}; | ||
this.plugin = plugin; | ||
this.provider = opts.provider; | ||
this.sharedHandler = new SharedHandler(plugin); | ||
this.isHandlingScroll = false; | ||
this.preFirstRender = this.preFirstRender.bind(this); | ||
this.handleError = this.handleError.bind(this); | ||
this.addFile = this.addFile.bind(this); | ||
this.clearSelection = this.clearSelection.bind(this); | ||
this.cancelPicking = this.cancelPicking.bind(this); | ||
} // eslint-disable-next-line class-methods-use-this | ||
providerFileToId(file) { | ||
return generateFileID({ | ||
data: file, | ||
name: file.name || file.id, | ||
type: file.mimetype | ||
}); | ||
} | ||
@@ -78,7 +144,8 @@ | ||
}, 'error', 5000); | ||
} | ||
} // todo document what is a "tagFile" or get rid of this concept | ||
addFile(file) { | ||
getTagFile(file) { | ||
const tagFile = { | ||
id: this.providerFileToId(file), | ||
id: file.id, | ||
source: this.plugin.id, | ||
@@ -100,3 +167,4 @@ data: file, | ||
providerOptions: this.provider.opts, | ||
providerName: this.provider.name | ||
providerName: this.provider.name, | ||
provider: this.provider.provider | ||
} | ||
@@ -115,16 +183,11 @@ }; | ||
this.plugin.uppy.log('Adding remote file'); | ||
return tagFile; | ||
} | ||
try { | ||
this.plugin.uppy.addFile(tagFile); | ||
return true; | ||
} catch (err) { | ||
if (!err.isRestriction) { | ||
this.plugin.uppy.log(err); | ||
} | ||
return false; | ||
} | ||
setLoading(loading) { | ||
this.plugin.setPluginState({ | ||
loading | ||
}); | ||
} | ||
} |
{ | ||
"name": "@uppy/provider-views", | ||
"description": "View library for Uppy remote provider plugins.", | ||
"version": "3.1.0", | ||
"version": "3.2.0", | ||
"license": "MIT", | ||
@@ -23,9 +23,10 @@ "main": "lib/index.js", | ||
"dependencies": { | ||
"@uppy/utils": "^5.1.3", | ||
"@uppy/utils": "^5.2.0", | ||
"classnames": "^2.2.6", | ||
"nanoid": "^4.0.0", | ||
"preact": "^10.5.13" | ||
}, | ||
"peerDependencies": { | ||
"@uppy/core": "^3.1.0" | ||
"@uppy/core": "^3.1.2" | ||
} | ||
} |
import { h } from 'preact' | ||
import classNames from 'classnames' | ||
import remoteFileObjToLocal from '@uppy/utils/lib/remoteFileObjToLocal' | ||
import Filter from './Filter.jsx' | ||
import SearchFilterInput from './SearchFilterInput.jsx' | ||
import FooterActions from './FooterActions.jsx' | ||
@@ -29,5 +27,10 @@ import Item from './Item/index.jsx' | ||
validateRestrictions, | ||
showFilter, | ||
filterQuery, | ||
filterInput, | ||
isLoading, | ||
showSearchFilter, | ||
search, | ||
searchTerm, | ||
clearSearch, | ||
searchOnInput, | ||
searchInputLabel, | ||
clearSearchLabel, | ||
getNextFolder, | ||
@@ -37,2 +40,3 @@ cancel, | ||
columns, | ||
noResultsLabel, | ||
} = props | ||
@@ -49,26 +53,42 @@ | ||
> | ||
<div className="uppy-ProviderBrowser-header"> | ||
<div | ||
className={classNames( | ||
'uppy-ProviderBrowser-headerBar', | ||
!showBreadcrumbs && 'uppy-ProviderBrowser-headerBar--simple', | ||
)} | ||
> | ||
{headerComponent} | ||
{headerComponent && ( | ||
<div className="uppy-ProviderBrowser-header"> | ||
<div | ||
className={classNames( | ||
'uppy-ProviderBrowser-headerBar', | ||
!showBreadcrumbs && 'uppy-ProviderBrowser-headerBar--simple', | ||
)} | ||
> | ||
{headerComponent} | ||
</div> | ||
</div> | ||
</div> | ||
)} | ||
{showFilter && ( | ||
<Filter | ||
i18n={i18n} | ||
filterQuery={filterQuery} | ||
filterInput={filterInput} | ||
/> | ||
{showSearchFilter && ( | ||
<div class="uppy-ProviderBrowser-searchFilter"> | ||
<SearchFilterInput | ||
search={search} | ||
searchTerm={searchTerm} | ||
clearSearch={clearSearch} | ||
inputLabel={searchInputLabel} | ||
clearSearchLabel={clearSearchLabel} | ||
inputClassName="uppy-ProviderBrowser-searchFilterInput" | ||
searchOnInput={searchOnInput} | ||
/> | ||
</div> | ||
)} | ||
{(() => { | ||
if (isLoading) { | ||
return ( | ||
<div className="uppy-Provider-loading"> | ||
<span>{i18n('loading')}</span> | ||
</div> | ||
) | ||
} | ||
if (!folders.length && !files.length) { | ||
return ( | ||
<div className="uppy-Provider-empty"> | ||
{i18n('noFilesFound')} | ||
{noResultsLabel} | ||
</div> | ||
@@ -75,0 +95,0 @@ ) |
import { h } from 'preact' | ||
import classNames from 'classnames' | ||
@@ -18,2 +19,9 @@ function GridListItem (props) { | ||
const checkBoxClassName = classNames( | ||
'uppy-u-reset', | ||
'uppy-ProviderBrowserItem-checkbox', | ||
'uppy-ProviderBrowserItem-checkbox--grid', | ||
{ 'uppy-ProviderBrowserItem-checkbox--is-checked': isChecked }, | ||
) | ||
return ( | ||
@@ -26,5 +34,3 @@ <li | ||
type="checkbox" | ||
className={`uppy-u-reset uppy-ProviderBrowserItem-checkbox ${ | ||
isChecked ? 'uppy-ProviderBrowserItem-checkbox--is-checked' : '' | ||
} uppy-ProviderBrowserItem-checkbox--grid`} | ||
className={checkBoxClassName} | ||
onChange={toggleCheckbox} | ||
@@ -43,9 +49,5 @@ onKeyDown={recordShiftKeyPress} | ||
> | ||
<span className="uppy-ProviderBrowserItem-inner-relative"> | ||
{itemIconEl} | ||
{showTitles && title} | ||
{children} | ||
</span> | ||
{itemIconEl} | ||
{showTitles && title} | ||
{children} | ||
</label> | ||
@@ -52,0 +54,0 @@ </li> |
@@ -45,2 +45,3 @@ import { h } from 'preact' | ||
className="uppy-ProviderBrowserItem-author" | ||
tabIndex="-1" | ||
> | ||
@@ -47,0 +48,0 @@ {author.name} |
import { h } from 'preact' | ||
import { getSafeFileId } from '@uppy/utils/lib/generateFileID' | ||
import AuthView from './AuthView.jsx' | ||
@@ -56,2 +58,3 @@ import Header from './Header.jsx' | ||
this.filterQuery = this.filterQuery.bind(this) | ||
this.clearFilter = this.clearFilter.bind(this) | ||
this.getFolder = this.getFolder.bind(this) | ||
@@ -62,3 +65,2 @@ this.getNextFolder = this.getNextFolder.bind(this) | ||
this.handleScroll = this.handleScroll.bind(this) | ||
this.listAllFiles = this.listAllFiles.bind(this) | ||
this.donePicking = this.donePicking.bind(this) | ||
@@ -105,25 +107,27 @@ | ||
*/ | ||
getFolder (id, name) { | ||
return this.sharedHandler.loaderWrapper( | ||
this.provider.list(id), | ||
(res) => { | ||
const folders = [] | ||
const files = [] | ||
let updatedDirectories | ||
async getFolder (id, name) { | ||
this.setLoading(true) | ||
try { | ||
const res = await this.provider.list(id) | ||
const folders = [] | ||
const files = [] | ||
let updatedDirectories | ||
const state = this.plugin.getPluginState() | ||
const index = state.directories.findIndex((dir) => id === dir.id) | ||
const state = this.plugin.getPluginState() | ||
const index = state.directories.findIndex((dir) => id === dir.id) | ||
if (index !== -1) { | ||
updatedDirectories = state.directories.slice(0, index + 1) | ||
} else { | ||
updatedDirectories = state.directories.concat([{ id, title: name }]) | ||
} | ||
if (index !== -1) { | ||
updatedDirectories = state.directories.slice(0, index + 1) | ||
} else { | ||
updatedDirectories = state.directories.concat([{ id, title: name }]) | ||
} | ||
this.username = res.username || this.username | ||
this.#updateFilesAndFolders(res, files, folders) | ||
this.plugin.setPluginState({ directories: updatedDirectories, filterInput: '' }) | ||
}, | ||
this.handleError, | ||
) | ||
this.username = res.username || this.username | ||
this.#updateFilesAndFolders(res, files, folders) | ||
this.plugin.setPluginState({ directories: updatedDirectories, filterInput: '' }) | ||
} catch (err) { | ||
this.handleError(err) | ||
} finally { | ||
this.setLoading(false) | ||
} | ||
} | ||
@@ -168,73 +172,8 @@ | ||
filterQuery (e) { | ||
const state = this.plugin.getPluginState() | ||
this.plugin.setPluginState({ ...state, filterInput: e ? e.target.value : '' }) | ||
filterQuery (input) { | ||
this.plugin.setPluginState({ filterInput: input }) | ||
} | ||
/** | ||
* Adds all files found inside of specified folder. | ||
* | ||
* Uses separated state while folder contents are being fetched and | ||
* mantains list of selected folders, which are separated from files. | ||
*/ | ||
addFolder (folder) { | ||
const folderId = this.providerFileToId(folder) | ||
const folders = { ...this.plugin.getPluginState().selectedFolders } | ||
if (folderId in folders && folders[folderId].loading) { | ||
return | ||
} | ||
folders[folderId] = { loading: true, files: [] } | ||
this.plugin.setPluginState({ selectedFolders: { ...folders } }) | ||
// eslint-disable-next-line consistent-return | ||
return this.listAllFiles(folder.requestPath).then((files) => { | ||
let count = 0 | ||
// If the same folder is added again, we don't want to send | ||
// X amount of duplicate file notifications, we want to say | ||
// the folder was already added. This checks if all files are duplicate, | ||
// if that's the case, we don't add the files. | ||
files.forEach(file => { | ||
const id = this.providerFileToId(file) | ||
if (!this.plugin.uppy.checkIfFileAlreadyExists(id)) { | ||
count++ | ||
} | ||
}) | ||
if (count > 0) { | ||
files.forEach((file) => this.addFile(file)) | ||
} | ||
const ids = files.map(this.providerFileToId) | ||
folders[folderId] = { | ||
loading: false, | ||
files: ids, | ||
} | ||
this.plugin.setPluginState({ selectedFolders: folders, filterInput: '' }) | ||
let message | ||
if (count === 0) { | ||
message = this.plugin.uppy.i18n('folderAlreadyAdded', { | ||
folder: folder.name, | ||
}) | ||
} else if (files.length) { | ||
message = this.plugin.uppy.i18n('folderAdded', { | ||
smart_count: count, folder: folder.name, | ||
}) | ||
} else { | ||
message = this.plugin.uppy.i18n('emptyFolderAdded') | ||
} | ||
this.plugin.uppy.info(message) | ||
}).catch((e) => { | ||
const selectedFolders = { ...this.plugin.getPluginState().selectedFolders } | ||
delete selectedFolders[folderId] | ||
this.plugin.setPluginState({ selectedFolders }) | ||
this.handleError(e) | ||
}) | ||
clearFilter () { | ||
this.plugin.setPluginState({ filterInput: '' }) | ||
} | ||
@@ -303,31 +242,87 @@ | ||
async listAllFiles (path, files = null) { | ||
files = files || [] // eslint-disable-line no-param-reassign | ||
const res = await this.provider.list(path) | ||
res.items.forEach((item) => { | ||
if (!item.isFolder) { | ||
files.push(item) | ||
} else { | ||
this.addFolder(item) | ||
async* recursivelyListAllFiles (path) { | ||
let curPath = path | ||
// need to repeat the list call until there are no more pages | ||
while (curPath) { | ||
const res = await this.provider.list(curPath) | ||
for (const item of res.items) { | ||
if (item.isFolder) { | ||
// recursively call self for folder | ||
yield* this.recursivelyListAllFiles(item.requestPath) | ||
} else { | ||
yield item | ||
} | ||
} | ||
}) | ||
const moreFiles = res.nextPagePath | ||
if (moreFiles) { | ||
return this.listAllFiles(moreFiles, files) | ||
curPath = res.nextPagePath | ||
} | ||
return files | ||
} | ||
donePicking () { | ||
const { currentSelection } = this.plugin.getPluginState() | ||
const promises = currentSelection.map((file) => { | ||
if (file.isFolder) { | ||
return this.addFolder(file) | ||
async donePicking () { | ||
this.setLoading(true) | ||
try { | ||
const { currentSelection } = this.plugin.getPluginState() | ||
const messages = [] | ||
const newFiles = [] | ||
// eslint-disable-next-line no-unreachable-loop | ||
for (const file of currentSelection) { | ||
if (file.isFolder) { | ||
const { requestPath, name } = file | ||
let isEmpty = true | ||
let numNewFiles = 0 | ||
for await (const fileInFolder of this.recursivelyListAllFiles(requestPath)) { | ||
const tagFile = this.getTagFile(fileInFolder) | ||
const id = getSafeFileId(tagFile) | ||
// If the same folder is added again, we don't want to send | ||
// X amount of duplicate file notifications, we want to say | ||
// the folder was already added. This checks if all files are duplicate, | ||
// if that's the case, we don't add the files. | ||
if (!this.plugin.uppy.checkIfFileAlreadyExists(id)) { | ||
newFiles.push(fileInFolder) | ||
numNewFiles++ | ||
} | ||
isEmpty = false | ||
} | ||
let message | ||
if (isEmpty) { | ||
message = this.plugin.uppy.i18n('emptyFolderAdded') | ||
} else if (numNewFiles === 0) { | ||
message = this.plugin.uppy.i18n('folderAlreadyAdded', { | ||
folder: name, | ||
}) | ||
} else { | ||
message = this.plugin.uppy.i18n('folderAdded', { | ||
smart_count: numNewFiles, folder: name, | ||
}) | ||
} | ||
messages.push(message) | ||
} else { | ||
newFiles.push(file) | ||
} | ||
} | ||
return this.addFile(file) | ||
}) | ||
this.sharedHandler.loaderWrapper(Promise.all(promises), () => { | ||
// Note: this.plugin.uppy.addFiles must be only run once we are done fetching all files, | ||
// because it will cause the loading screen to disappear, | ||
// and that will allow the user to start the upload, so we need to make sure we have | ||
// finished all async operations before we add any file | ||
// see https://github.com/transloadit/uppy/pull/4384 | ||
this.plugin.uppy.log('Adding remote provider files') | ||
this.plugin.uppy.addFiles(newFiles.map((file) => this.getTagFile(file))) | ||
this.plugin.setPluginState({ filterInput: '' }) | ||
messages.forEach(message => this.plugin.uppy.info(message)) | ||
this.clearSelection() | ||
}, () => {}) | ||
} catch (err) { | ||
this.handleError(err) | ||
} finally { | ||
this.setLoading(false) | ||
} | ||
} | ||
@@ -337,2 +332,3 @@ | ||
const { authenticated, didFirstRender } = this.plugin.getPluginState() | ||
const { i18n } = this.plugin.uppy | ||
@@ -345,3 +341,3 @@ if (!didFirstRender) { | ||
const { files, folders, filterInput, loading, currentSelection } = this.plugin.getPluginState() | ||
const { isChecked, toggleCheckbox, recordShiftKeyPress, filterItems } = this.sharedHandler | ||
const { isChecked, toggleCheckbox, recordShiftKeyPress, filterItems } = this | ||
const hasInput = filterInput !== '' | ||
@@ -356,3 +352,3 @@ const headerProps = { | ||
username: this.username, | ||
i18n: this.plugin.uppy.i18n, | ||
i18n, | ||
} | ||
@@ -370,7 +366,15 @@ | ||
getFolder: this.getFolder, | ||
filterItems: this.sharedHandler.filterItems, | ||
filterQuery: this.filterQuery, | ||
// For SearchFilterInput component | ||
showSearchFilter: targetViewOptions.showFilter, | ||
search: this.filterQuery, | ||
clearSearch: this.clearFilter, | ||
searchTerm: filterInput, | ||
searchOnInput: true, | ||
searchInputLabel: i18n('filter'), | ||
clearSearchLabel: i18n('resetFilter'), | ||
noResultsLabel: i18n('noFilesFound'), | ||
logout: this.logout, | ||
handleScroll: this.handleScroll, | ||
listAllFiles: this.listAllFiles, | ||
done: this.donePicking, | ||
@@ -382,3 +386,2 @@ cancel: this.cancelPicking, | ||
showTitles: targetViewOptions.showTitles, | ||
showFilter: targetViewOptions.showFilter, | ||
showBreadcrumbs: targetViewOptions.showBreadcrumbs, | ||
@@ -385,0 +388,0 @@ pluginIcon: this.plugin.icon, |
import { h } from 'preact' | ||
import SearchInput from './InputView.jsx' | ||
import SearchFilterInput from '../SearchFilterInput.jsx' | ||
import Browser from '../Browser.jsx' | ||
import LoaderView from '../Loader.jsx' | ||
import Header from './Header.jsx' | ||
import CloseWrapper from '../CloseWrapper.js' | ||
@@ -13,3 +11,4 @@ import View from '../View.js' | ||
/** | ||
* Class to easily generate generic views for Provider plugins | ||
* SearchProviderView, used for Unsplash and future image search providers. | ||
* Extends generic View, shared with regular providers like Google Drive and Instagram. | ||
*/ | ||
@@ -39,4 +38,4 @@ export default class SearchProviderView extends View { | ||
this.search = this.search.bind(this) | ||
this.triggerSearchInput = this.triggerSearchInput.bind(this) | ||
this.addFile = this.addFile.bind(this) | ||
this.clearSearch = this.clearSearch.bind(this) | ||
this.resetPluginState = this.resetPluginState.bind(this) | ||
this.handleScroll = this.handleScroll.bind(this) | ||
@@ -48,4 +47,3 @@ this.donePicking = this.donePicking.bind(this) | ||
// Set default state for the plugin | ||
this.plugin.setPluginState({ | ||
this.defaultState = { | ||
isInputMode: true, | ||
@@ -58,3 +56,6 @@ files: [], | ||
searchTerm: null, | ||
}) | ||
} | ||
// Set default state for the plugin | ||
this.plugin.setPluginState(this.defaultState) | ||
} | ||
@@ -67,9 +68,4 @@ | ||
clearSelection () { | ||
this.plugin.setPluginState({ | ||
currentSelection: [], | ||
isInputMode: true, | ||
files: [], | ||
searchTerm: null, | ||
}) | ||
resetPluginState () { | ||
this.plugin.setPluginState(this.defaultState) | ||
} | ||
@@ -81,2 +77,3 @@ | ||
this.plugin.setPluginState({ | ||
currentSelection: [], | ||
isInputMode: false, | ||
@@ -88,20 +85,26 @@ files, | ||
search (query) { | ||
async search (query) { | ||
const { searchTerm } = this.plugin.getPluginState() | ||
if (query && query === searchTerm) { | ||
// no need to search again as this is the same as the previous search | ||
return undefined | ||
return | ||
} | ||
return this.sharedHandler.loaderWrapper( | ||
this.provider.search(query), | ||
(res) => { | ||
this.#updateFilesAndInputMode(res, []) | ||
}, | ||
this.handleError, | ||
) | ||
this.setLoading(true) | ||
try { | ||
const res = await this.provider.search(query) | ||
this.#updateFilesAndInputMode(res, []) | ||
} catch (err) { | ||
this.handleError(err) | ||
} finally { | ||
this.setLoading(false) | ||
} | ||
} | ||
triggerSearchInput () { | ||
this.plugin.setPluginState({ isInputMode: true }) | ||
clearSearch () { | ||
this.plugin.setPluginState({ | ||
currentSelection: [], | ||
files: [], | ||
searchTerm: null, | ||
}) | ||
} | ||
@@ -130,7 +133,5 @@ | ||
const { currentSelection } = this.plugin.getPluginState() | ||
const promises = currentSelection.map((file) => this.addFile(file)) | ||
this.sharedHandler.loaderWrapper(Promise.all(promises), () => { | ||
this.clearSelection() | ||
}, () => {}) | ||
this.plugin.uppy.log('Adding remote search provider files') | ||
this.plugin.uppy.addFiles(currentSelection.map((file) => this.getTagFile(file))) | ||
this.resetPluginState() | ||
} | ||
@@ -140,2 +141,3 @@ | ||
const { didFirstRender, isInputMode, searchTerm } = this.plugin.getPluginState() | ||
const { i18n } = this.plugin.uppy | ||
@@ -148,3 +150,3 @@ if (!didFirstRender) { | ||
const { files, folders, filterInput, loading, currentSelection } = this.plugin.getPluginState() | ||
const { isChecked, toggleCheckbox, filterItems } = this.sharedHandler | ||
const { isChecked, toggleCheckbox, filterItems } = this | ||
const hasInput = filterInput !== '' | ||
@@ -161,7 +163,13 @@ | ||
cancel: this.cancelPicking, | ||
headerComponent: Header({ | ||
search: this.search, | ||
i18n: this.plugin.uppy.i18n, | ||
searchTerm, | ||
}), | ||
// For SearchFilterInput component | ||
showSearchFilter: targetViewOptions.showFilter, | ||
search: this.search, | ||
clearSearch: this.clearSearch, | ||
searchTerm, | ||
searchOnInput: false, | ||
searchInputLabel: i18n('search'), | ||
clearSearchLabel: i18n('resetSearch'), | ||
noResultsLabel: i18n('noSearchResults'), | ||
title: this.plugin.title, | ||
@@ -171,5 +179,6 @@ viewType: targetViewOptions.viewType, | ||
showFilter: targetViewOptions.showFilter, | ||
isLoading: loading, | ||
showBreadcrumbs: targetViewOptions.showBreadcrumbs, | ||
pluginIcon: this.plugin.icon, | ||
i18n: this.plugin.uppy.i18n, | ||
i18n, | ||
uppyFiles: this.plugin.uppy.getFiles(), | ||
@@ -179,17 +188,16 @@ validateRestrictions: (...args) => this.plugin.uppy.validateRestrictions(...args), | ||
if (loading) { | ||
return ( | ||
<CloseWrapper onUnmount={this.clearSelection}> | ||
<LoaderView i18n={this.plugin.uppy.i18n} /> | ||
</CloseWrapper> | ||
) | ||
} | ||
if (isInputMode) { | ||
return ( | ||
<CloseWrapper onUnmount={this.clearSelection}> | ||
<SearchInput | ||
search={this.search} | ||
i18n={this.plugin.uppy.i18n} | ||
/> | ||
<CloseWrapper onUnmount={this.resetPluginState}> | ||
<div className="uppy-SearchProvider"> | ||
<SearchFilterInput | ||
search={this.search} | ||
clearSelection={this.clearSelection} | ||
inputLabel={i18n('enterTextToSearch')} | ||
buttonLabel={i18n('searchImages')} | ||
inputClassName="uppy-c-textInput uppy-SearchProvider-input" | ||
buttonCSSClassName="uppy-SearchProvider-searchButton" | ||
showButton | ||
/> | ||
</div> | ||
</CloseWrapper> | ||
@@ -200,3 +208,3 @@ ) | ||
return ( | ||
<CloseWrapper onUnmount={this.clearSelection}> | ||
<CloseWrapper onUnmount={this.resetPluginState}> | ||
{/* eslint-disable-next-line react/jsx-props-no-spreading */} | ||
@@ -203,0 +211,0 @@ <Browser {...browserProps} /> |
109
src/View.js
import getFileType from '@uppy/utils/lib/getFileType' | ||
import isPreviewSupported from '@uppy/utils/lib/isPreviewSupported' | ||
import generateFileID from '@uppy/utils/lib/generateFileID' | ||
import remoteFileObjToLocal from '@uppy/utils/lib/remoteFileObjToLocal' | ||
// TODO: now that we have a shared `View` class, | ||
// `SharedHandler` could be cleaned up and moved into here | ||
import SharedHandler from './SharedHandler.js' | ||
export default class View { | ||
@@ -13,3 +9,2 @@ constructor (plugin, opts) { | ||
this.provider = opts.provider | ||
this.sharedHandler = new SharedHandler(plugin) | ||
@@ -20,3 +15,2 @@ this.isHandlingScroll = false | ||
this.handleError = this.handleError.bind(this) | ||
this.addFile = this.addFile.bind(this) | ||
this.clearSelection = this.clearSelection.bind(this) | ||
@@ -26,11 +20,2 @@ this.cancelPicking = this.cancelPicking.bind(this) | ||
// eslint-disable-next-line class-methods-use-this | ||
providerFileToId (file) { | ||
return generateFileID({ | ||
data: file, | ||
name: file.name || file.id, | ||
type: file.mimetype, | ||
}) | ||
} | ||
preFirstRender () { | ||
@@ -76,5 +61,6 @@ this.plugin.setPluginState({ didFirstRender: true }) | ||
addFile (file) { | ||
// todo document what is a "tagFile" or get rid of this concept | ||
getTagFile (file) { | ||
const tagFile = { | ||
id: this.providerFileToId(file), | ||
id: file.id, | ||
source: this.plugin.id, | ||
@@ -97,2 +83,3 @@ data: file, | ||
providerName: this.provider.name, | ||
provider: this.provider.provider, | ||
}, | ||
@@ -113,14 +100,84 @@ } | ||
this.plugin.uppy.log('Adding remote file') | ||
return tagFile | ||
} | ||
try { | ||
this.plugin.uppy.addFile(tagFile) | ||
return true | ||
} catch (err) { | ||
if (!err.isRestriction) { | ||
this.plugin.uppy.log(err) | ||
filterItems = (items) => { | ||
const state = this.plugin.getPluginState() | ||
if (!state.filterInput || state.filterInput === '') { | ||
return items | ||
} | ||
return items.filter((folder) => { | ||
return folder.name.toLowerCase().indexOf(state.filterInput.toLowerCase()) !== -1 | ||
}) | ||
} | ||
recordShiftKeyPress = (e) => { | ||
this.isShiftKeyPressed = e.shiftKey | ||
} | ||
/** | ||
* 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. | ||
*/ | ||
toggleCheckbox = (e, file) => { | ||
e.stopPropagation() | ||
e.preventDefault() | ||
e.currentTarget.focus() | ||
const { folders, files } = this.plugin.getPluginState() | ||
const 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 && this.isShiftKeyPressed) { | ||
const prevIndex = items.indexOf(this.lastCheckbox) | ||
const currentIndex = items.indexOf(file) | ||
const currentSelection = (prevIndex < currentIndex) | ||
? items.slice(prevIndex, currentIndex + 1) | ||
: items.slice(currentIndex, prevIndex + 1) | ||
const reducedCurrentSelection = [] | ||
// Check restrictions on each file in currentSelection, | ||
// reduce it to only contain files that pass restrictions | ||
for (const item of currentSelection) { | ||
const { uppy } = this.plugin | ||
const restrictionError = uppy.validateRestrictions( | ||
remoteFileObjToLocal(item), | ||
[...uppy.getFiles(), ...reducedCurrentSelection], | ||
) | ||
if (!restrictionError) { | ||
reducedCurrentSelection.push(item) | ||
} else { | ||
uppy.info({ message: restrictionError.message }, 'error', uppy.opts.infoTimeout) | ||
} | ||
} | ||
return false | ||
this.plugin.setPluginState({ currentSelection: reducedCurrentSelection }) | ||
return | ||
} | ||
this.lastCheckbox = file | ||
const { currentSelection } = this.plugin.getPluginState() | ||
if (this.isChecked(file)) { | ||
this.plugin.setPluginState({ | ||
currentSelection: currentSelection.filter((item) => item.id !== file.id), | ||
}) | ||
} else { | ||
this.plugin.setPluginState({ | ||
currentSelection: currentSelection.concat([file]), | ||
}) | ||
} | ||
} | ||
isChecked = (file) => { | ||
const { currentSelection } = this.plugin.getPluginState() | ||
// comparing id instead of the file object, because the reference to the object | ||
// changes when we switch folders, and the file list is updated | ||
return currentSelection.some((item) => item.id === file.id) | ||
} | ||
setLoading (loading) { | ||
this.plugin.setPluginState({ loading }) | ||
} | ||
} |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
235976
5
69
3391
+ Addednanoid@^4.0.0
Updated@uppy/utils@^5.2.0