@uppy/provider-views
Advanced tools
Comparing version 3.4.0 to 3.4.1
# @uppy/provider-views | ||
## 3.4.1 | ||
Released: 2023-07-20 | ||
Included in: Uppy v3.13.0 | ||
- @uppy/provider-views: Add VirtualList to ProviderView (Merlijn Vos / #4566) | ||
- @uppy/provider-views: fix race conditions with folder loading (Mikael Finstad / #4578) | ||
- @uppy/provider-views: fix infinite folder loading (Mikael Finstad / #4590) | ||
## 3.4.0 | ||
@@ -4,0 +13,0 @@ |
import { h } from 'preact'; | ||
import classNames from 'classnames'; | ||
import remoteFileObjToLocal from '@uppy/utils/lib/remoteFileObjToLocal'; | ||
import { useMemo } from 'preact/hooks'; | ||
import VirtualList from '@uppy/utils/lib/VirtualList'; | ||
import SearchFilterInput from "./SearchFilterInput.js"; | ||
@@ -8,2 +10,54 @@ import FooterActions from "./FooterActions.js"; | ||
const VIRTUAL_SHARED_DIR = 'shared-with-me'; | ||
function ListItem(props) { | ||
const { | ||
currentSelection, | ||
uppyFiles, | ||
viewType, | ||
isChecked, | ||
toggleCheckbox, | ||
recordShiftKeyPress, | ||
showTitles, | ||
i18n, | ||
validateRestrictions, | ||
getNextFolder, | ||
columns, | ||
f | ||
} = props; | ||
if (f.isFolder) { | ||
var _isChecked; | ||
return Item({ | ||
columns, | ||
showTitles, | ||
viewType, | ||
i18n, | ||
id: f.id, | ||
title: f.name, | ||
getItemIcon: () => f.icon, | ||
isChecked: isChecked(f), | ||
toggleCheckbox: event => toggleCheckbox(event, f), | ||
recordShiftKeyPress, | ||
type: 'folder', | ||
isDisabled: (_isChecked = isChecked(f)) == null ? void 0 : _isChecked.loading, | ||
isCheckboxDisabled: f.id === VIRTUAL_SHARED_DIR, | ||
handleFolderClick: () => getNextFolder(f) | ||
}); | ||
} | ||
const restrictionError = validateRestrictions(remoteFileObjToLocal(f), [...uppyFiles, ...currentSelection]); | ||
return Item({ | ||
id: f.id, | ||
title: f.name, | ||
author: f.author, | ||
getItemIcon: () => f.icon, | ||
isChecked: isChecked(f), | ||
toggleCheckbox: event => toggleCheckbox(event, f), | ||
recordShiftKeyPress, | ||
columns, | ||
showTitles, | ||
viewType, | ||
i18n, | ||
type: 'file', | ||
isDisabled: restrictionError && !isChecked(f), | ||
restrictionError | ||
}); | ||
} | ||
function Browser(props) { | ||
@@ -37,5 +91,7 @@ const { | ||
columns, | ||
noResultsLabel | ||
noResultsLabel, | ||
loadAllFiles | ||
} = props; | ||
const selected = currentSelection.length; | ||
const rows = useMemo(() => [...folders, ...files], [folders, files]); | ||
return h("div", { | ||
@@ -68,2 +124,26 @@ className: classNames('uppy-ProviderBrowser', `uppy-ProviderBrowser-viewType--${viewType}`) | ||
} | ||
if (loadAllFiles) { | ||
return h("div", { | ||
className: "uppy-ProviderBrowser-body" | ||
}, h("ul", { | ||
className: "uppy-ProviderBrowser-list" | ||
}, h(VirtualList, { | ||
data: rows, | ||
renderRow: f => h(ListItem, { | ||
currentSelection: currentSelection, | ||
uppyFiles: uppyFiles, | ||
viewType: viewType, | ||
isChecked: isChecked, | ||
toggleCheckbox: toggleCheckbox, | ||
recordShiftKeyPress: recordShiftKeyPress, | ||
showTitles: showTitles, | ||
i18n: i18n, | ||
validateRestrictions: validateRestrictions, | ||
getNextFolder: getNextFolder, | ||
columns: columns, | ||
f: f | ||
}), | ||
rowHeight: 31 | ||
}))); | ||
} | ||
return h("div", { | ||
@@ -78,39 +158,16 @@ className: "uppy-ProviderBrowser-body" | ||
tabIndex: "-1" | ||
}, folders.map(folder => { | ||
var _isChecked; | ||
return Item({ | ||
columns, | ||
showTitles, | ||
viewType, | ||
i18n, | ||
id: folder.id, | ||
title: folder.name, | ||
getItemIcon: () => folder.icon, | ||
isChecked: isChecked(folder), | ||
toggleCheckbox: event => toggleCheckbox(event, folder), | ||
recordShiftKeyPress, | ||
type: 'folder', | ||
isDisabled: (_isChecked = isChecked(folder)) == null ? void 0 : _isChecked.loading, | ||
isCheckboxDisabled: folder.id === VIRTUAL_SHARED_DIR, | ||
handleFolderClick: () => getNextFolder(folder) | ||
}); | ||
}), files.map(file => { | ||
const restrictionError = validateRestrictions(remoteFileObjToLocal(file), [...uppyFiles, ...currentSelection]); | ||
return Item({ | ||
id: file.id, | ||
title: file.name, | ||
author: file.author, | ||
getItemIcon: () => file.icon, | ||
isChecked: isChecked(file), | ||
toggleCheckbox: event => toggleCheckbox(event, file), | ||
recordShiftKeyPress, | ||
columns, | ||
showTitles, | ||
viewType, | ||
i18n, | ||
type: 'file', | ||
isDisabled: restrictionError && !isChecked(file), | ||
restrictionError | ||
}); | ||
}))); | ||
}, rows.map(f => h(ListItem, { | ||
currentSelection: currentSelection, | ||
uppyFiles: uppyFiles, | ||
viewType: viewType, | ||
isChecked: isChecked, | ||
toggleCheckbox: toggleCheckbox, | ||
recordShiftKeyPress: recordShiftKeyPress, | ||
showTitles: showTitles, | ||
i18n: i18n, | ||
validateRestrictions: validateRestrictions, | ||
getNextFolder: getNextFolder, | ||
columns: columns, | ||
f: f | ||
})))); | ||
})(), selected > 0 && h(FooterActions, { | ||
@@ -117,0 +174,0 @@ selected: selected, |
@@ -63,3 +63,5 @@ import { h } from 'preact'; | ||
alt: alt, | ||
loading: "lazy" | ||
loading: "lazy", | ||
width: 16, | ||
height: 16 | ||
}); | ||
@@ -66,0 +68,0 @@ } |
@@ -15,3 +15,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; } | ||
const packageJson = { | ||
"version": "3.4.0" | ||
"version": "3.4.1" | ||
}; | ||
@@ -47,2 +47,4 @@ function getOrigin() { | ||
*/ | ||
var _abortController = /*#__PURE__*/_classPrivateFieldLooseKey("abortController"); | ||
var _withAbort = /*#__PURE__*/_classPrivateFieldLooseKey("withAbort"); | ||
var _list = /*#__PURE__*/_classPrivateFieldLooseKey("list"); | ||
@@ -68,2 +70,9 @@ var _listFilesAndFolders = /*#__PURE__*/_classPrivateFieldLooseKey("listFilesAndFolders"); | ||
}); | ||
Object.defineProperty(this, _withAbort, { | ||
value: _withAbort2 | ||
}); | ||
Object.defineProperty(this, _abortController, { | ||
writable: true, | ||
value: void 0 | ||
}); | ||
const defaultOptions = { | ||
@@ -122,60 +131,48 @@ viewType: 'list', | ||
async getFolder(requestPath, name) { | ||
const controller = new AbortController(); | ||
const cancelRequest = () => { | ||
controller.abort(); | ||
this.clearSelection(); | ||
}; | ||
this.plugin.uppy.on('dashboard:close-panel', cancelRequest); | ||
this.plugin.uppy.on('cancel-all', cancelRequest); | ||
this.setLoading(true); | ||
try { | ||
this.lastCheckbox = undefined; | ||
let { | ||
breadcrumbs | ||
} = this.plugin.getPluginState(); | ||
const index = breadcrumbs.findIndex(dir => requestPath === dir.requestPath); | ||
if (index !== -1) { | ||
// means we navigated back to a known directory (already in the stack), so cut the stack off there | ||
breadcrumbs = breadcrumbs.slice(0, index + 1); | ||
} else { | ||
// we have navigated into a new (unknown) folder, add it to the stack | ||
breadcrumbs = [...breadcrumbs, { | ||
requestPath, | ||
name | ||
}]; | ||
} | ||
let files = []; | ||
let folders = []; | ||
do { | ||
const { | ||
files: newFiles, | ||
folders: newFolders | ||
} = await _classPrivateFieldLooseBase(this, _listFilesAndFolders)[_listFilesAndFolders]({ | ||
requestPath, | ||
await _classPrivateFieldLooseBase(this, _withAbort)[_withAbort](async signal => { | ||
this.lastCheckbox = undefined; | ||
let { | ||
breadcrumbs | ||
} = this.plugin.getPluginState(); | ||
const index = breadcrumbs.findIndex(dir => requestPath === dir.requestPath); | ||
if (index !== -1) { | ||
// means we navigated back to a known directory (already in the stack), so cut the stack off there | ||
breadcrumbs = breadcrumbs.slice(0, index + 1); | ||
} else { | ||
// we have navigated into a new (unknown) folder, add it to the stack | ||
breadcrumbs = [...breadcrumbs, { | ||
requestPath, | ||
name | ||
}]; | ||
} | ||
this.nextPagePath = requestPath; | ||
let files = []; | ||
let folders = []; | ||
do { | ||
const { | ||
files: newFiles, | ||
folders: newFolders | ||
} = await _classPrivateFieldLooseBase(this, _listFilesAndFolders)[_listFilesAndFolders]({ | ||
breadcrumbs, | ||
signal | ||
}); | ||
files = files.concat(newFiles); | ||
folders = folders.concat(newFolders); | ||
this.setLoading(this.plugin.uppy.i18n('loadedXFiles', { | ||
numFiles: files.length + folders.length | ||
})); | ||
} while (this.opts.loadAllFiles && this.nextPagePath); | ||
this.plugin.setPluginState({ | ||
folders, | ||
files, | ||
breadcrumbs, | ||
signal: controller.signal | ||
filterInput: '' | ||
}); | ||
files = files.concat(newFiles); | ||
folders = folders.concat(newFolders); | ||
this.setLoading(this.plugin.uppy.i18n('loadedXFiles', { | ||
numFiles: files.length + folders.length | ||
})); | ||
} while (this.opts.loadAllFiles && this.nextPagePath); | ||
this.plugin.setPluginState({ | ||
folders, | ||
files, | ||
breadcrumbs, | ||
filterInput: '' | ||
}); | ||
} catch (err) { | ||
var _err$cause; | ||
if (((_err$cause = err.cause) == null ? void 0 : _err$cause.name) === 'AbortError') { | ||
// Expected, user clicked “cancel” | ||
return; | ||
} | ||
this.handleError(err); | ||
} finally { | ||
this.setLoading(false); | ||
this.plugin.uppy.off('dashboard:close-panel', cancelRequest); | ||
this.plugin.uppy.off('cancel-all', cancelRequest); | ||
} | ||
@@ -197,22 +194,29 @@ } | ||
*/ | ||
logout() { | ||
this.provider.logout().then(res => { | ||
if (res.ok) { | ||
if (!res.revoked) { | ||
const message = this.plugin.uppy.i18n('companionUnauthorizeHint', { | ||
provider: this.plugin.title, | ||
url: res.manual_revoke_url | ||
}); | ||
this.plugin.uppy.info(message, 'info', 7000); | ||
async logout() { | ||
try { | ||
await _classPrivateFieldLooseBase(this, _withAbort)[_withAbort](async signal => { | ||
const res = await this.provider.logout({ | ||
signal | ||
}); | ||
if (res.ok) { | ||
if (!res.revoked) { | ||
const message = this.plugin.uppy.i18n('companionUnauthorizeHint', { | ||
provider: this.plugin.title, | ||
url: res.manual_revoke_url | ||
}); | ||
this.plugin.uppy.info(message, 'info', 7000); | ||
} | ||
const newState = { | ||
authenticated: false, | ||
files: [], | ||
folders: [], | ||
breadcrumbs: [], | ||
filterInput: '' | ||
}; | ||
this.plugin.setPluginState(newState); | ||
} | ||
const newState = { | ||
authenticated: false, | ||
files: [], | ||
folders: [], | ||
breadcrumbs: [], | ||
filterInput: '' | ||
}; | ||
this.plugin.setPluginState(newState); | ||
} | ||
}).catch(this.handleError); | ||
}); | ||
} catch (err) { | ||
this.handleError(err); | ||
} | ||
} | ||
@@ -275,24 +279,25 @@ filterQuery(input) { | ||
async handleScroll(event) { | ||
const requestPath = this.nextPagePath || null; | ||
if (this.shouldHandleScroll(event) && requestPath) { | ||
if (this.shouldHandleScroll(event) && this.nextPagePath) { | ||
this.isHandlingScroll = true; | ||
try { | ||
const { | ||
files, | ||
folders, | ||
breadcrumbs | ||
} = this.plugin.getPluginState(); | ||
const { | ||
files: newFiles, | ||
folders: newFolders | ||
} = await _classPrivateFieldLooseBase(this, _listFilesAndFolders)[_listFilesAndFolders]({ | ||
requestPath, | ||
breadcrumbs | ||
await _classPrivateFieldLooseBase(this, _withAbort)[_withAbort](async signal => { | ||
const { | ||
files, | ||
folders, | ||
breadcrumbs | ||
} = this.plugin.getPluginState(); | ||
const { | ||
files: newFiles, | ||
folders: newFolders | ||
} = await _classPrivateFieldLooseBase(this, _listFilesAndFolders)[_listFilesAndFolders]({ | ||
breadcrumbs, | ||
signal | ||
}); | ||
const combinedFiles = files.concat(newFiles); | ||
const combinedFolders = folders.concat(newFolders); | ||
this.plugin.setPluginState({ | ||
folders: combinedFolders, | ||
files: combinedFiles | ||
}); | ||
}); | ||
const combinedFiles = files.concat(newFiles); | ||
const combinedFolders = folders.concat(newFolders); | ||
this.plugin.setPluginState({ | ||
folders: combinedFolders, | ||
files: combinedFiles | ||
}); | ||
} catch (error) { | ||
@@ -308,83 +313,86 @@ this.handleError(error); | ||
try { | ||
const { | ||
currentSelection | ||
} = this.plugin.getPluginState(); | ||
const messages = []; | ||
const newFiles = []; | ||
for (const selectedItem of currentSelection) { | ||
await _classPrivateFieldLooseBase(this, _withAbort)[_withAbort](async signal => { | ||
const { | ||
requestPath | ||
} = selectedItem; | ||
const withRelDirPath = newItem => ({ | ||
...newItem, | ||
// calculate the file's path relative to the user's selected item's path | ||
// see https://github.com/transloadit/uppy/pull/4537#issuecomment-1614236655 | ||
relDirPath: newItem.absDirPath.replace(selectedItem.absDirPath, '').replace(/^\//, '') | ||
}); | ||
if (selectedItem.isFolder) { | ||
let isEmpty = true; | ||
let numNewFiles = 0; | ||
const queue = new PQueue({ | ||
concurrency: 6 | ||
currentSelection | ||
} = this.plugin.getPluginState(); | ||
const messages = []; | ||
const newFiles = []; | ||
for (const selectedItem of currentSelection) { | ||
const { | ||
requestPath | ||
} = selectedItem; | ||
const withRelDirPath = newItem => ({ | ||
...newItem, | ||
// calculate the file's path relative to the user's selected item's path | ||
// see https://github.com/transloadit/uppy/pull/4537#issuecomment-1614236655 | ||
relDirPath: newItem.absDirPath.replace(selectedItem.absDirPath, '').replace(/^\//, '') | ||
}); | ||
const onFiles = files => { | ||
for (const newFile of files) { | ||
const tagFile = this.getTagFile(newFile); | ||
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(withRelDirPath(newFile)); | ||
numNewFiles++; | ||
this.setLoading(this.plugin.uppy.i18n('addedNumFiles', { | ||
numFiles: numNewFiles | ||
})); | ||
if (selectedItem.isFolder) { | ||
let isEmpty = true; | ||
let numNewFiles = 0; | ||
const queue = new PQueue({ | ||
concurrency: 6 | ||
}); | ||
const onFiles = files => { | ||
for (const newFile of files) { | ||
const tagFile = this.getTagFile(newFile); | ||
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(withRelDirPath(newFile)); | ||
numNewFiles++; | ||
this.setLoading(this.plugin.uppy.i18n('addedNumFiles', { | ||
numFiles: numNewFiles | ||
})); | ||
} | ||
isEmpty = false; | ||
} | ||
isEmpty = false; | ||
}; | ||
await _classPrivateFieldLooseBase(this, _recursivelyListAllFiles)[_recursivelyListAllFiles]({ | ||
requestPath, | ||
absDirPath: prependPath(selectedItem.absDirPath, selectedItem.name), | ||
relDirPath: selectedItem.name, | ||
queue, | ||
onFiles, | ||
signal | ||
}); | ||
await queue.onIdle(); | ||
let message; | ||
if (isEmpty) { | ||
message = this.plugin.uppy.i18n('emptyFolderAdded'); | ||
} else if (numNewFiles === 0) { | ||
message = this.plugin.uppy.i18n('folderAlreadyAdded', { | ||
folder: selectedItem.name | ||
}); | ||
} else { | ||
// TODO we don't really know at this point whether any files were actually added | ||
// (only later after addFiles has been called) so we should probably rewrite this. | ||
// Example: If all files fail to add due to restriction error, it will still say "Added 100 files from folder" | ||
message = this.plugin.uppy.i18n('folderAdded', { | ||
smart_count: numNewFiles, | ||
folder: selectedItem.name | ||
}); | ||
} | ||
}; | ||
await _classPrivateFieldLooseBase(this, _recursivelyListAllFiles)[_recursivelyListAllFiles]({ | ||
requestPath, | ||
absDirPath: prependPath(selectedItem.absDirPath, selectedItem.name), | ||
relDirPath: selectedItem.name, | ||
queue, | ||
onFiles | ||
}); | ||
await queue.onIdle(); | ||
let message; | ||
if (isEmpty) { | ||
message = this.plugin.uppy.i18n('emptyFolderAdded'); | ||
} else if (numNewFiles === 0) { | ||
message = this.plugin.uppy.i18n('folderAlreadyAdded', { | ||
folder: selectedItem.name | ||
}); | ||
messages.push(message); | ||
} else { | ||
// TODO we don't really know at this point whether any files were actually added | ||
// (only later after addFiles has been called) so we should probably rewrite this. | ||
// Example: If all files fail to add due to restriction error, it will still say "Added 100 files from folder" | ||
message = this.plugin.uppy.i18n('folderAdded', { | ||
smart_count: numNewFiles, | ||
folder: selectedItem.name | ||
}); | ||
newFiles.push(withRelDirPath(selectedItem)); | ||
} | ||
messages.push(message); | ||
} else { | ||
newFiles.push(withRelDirPath(selectedItem)); | ||
} | ||
} | ||
// 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: '' | ||
// 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(); | ||
}); | ||
messages.forEach(message => this.plugin.uppy.info(message)); | ||
this.clearSelection(); | ||
} catch (err) { | ||
@@ -449,2 +457,3 @@ this.handleError(err); | ||
getFolder: this.getFolder, | ||
loadAllFiles: this.opts.loadAllFiles, | ||
// For SearchFilterInput component | ||
@@ -499,2 +508,22 @@ showSearchFilter: targetViewOptions.showFilter, | ||
} | ||
async function _withAbort2(op) { | ||
var _classPrivateFieldLoo; | ||
// prevent multiple requests in parallel from causing race conditions | ||
(_classPrivateFieldLoo = _classPrivateFieldLooseBase(this, _abortController)[_abortController]) == null ? void 0 : _classPrivateFieldLoo.abort(); | ||
const abortController = new AbortController(); | ||
_classPrivateFieldLooseBase(this, _abortController)[_abortController] = abortController; | ||
const cancelRequest = () => { | ||
abortController.abort(); | ||
this.clearSelection(); | ||
}; | ||
try { | ||
this.plugin.uppy.on('dashboard:close-panel', cancelRequest); | ||
this.plugin.uppy.on('cancel-all', cancelRequest); | ||
await op(abortController.signal); | ||
} finally { | ||
this.plugin.uppy.off('dashboard:close-panel', cancelRequest); | ||
this.plugin.uppy.off('cancel-all', cancelRequest); | ||
_classPrivateFieldLooseBase(this, _abortController)[_abortController] = undefined; | ||
} | ||
} | ||
async function _list2(_ref) { | ||
@@ -524,3 +553,2 @@ let { | ||
let { | ||
requestPath, | ||
breadcrumbs, | ||
@@ -534,3 +562,3 @@ signal | ||
} = await _classPrivateFieldLooseBase(this, _list)[_list]({ | ||
requestPath, | ||
requestPath: this.nextPagePath, | ||
absDirPath, | ||
@@ -560,3 +588,4 @@ signal | ||
queue, | ||
onFiles | ||
onFiles, | ||
signal | ||
} = _ref3; | ||
@@ -567,3 +596,4 @@ let curPath = requestPath; | ||
requestPath: curPath, | ||
absDirPath | ||
absDirPath, | ||
signal | ||
}); | ||
@@ -581,3 +611,4 @@ curPath = res.nextPagePath; | ||
queue, | ||
onFiles | ||
onFiles, | ||
signal | ||
}))); | ||
@@ -584,0 +615,0 @@ await Promise.all(promises); // in case we get an error |
@@ -10,3 +10,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; } | ||
const packageJson = { | ||
"version": "3.4.0" | ||
"version": "3.4.1" | ||
}; | ||
@@ -13,0 +13,0 @@ /** |
@@ -126,2 +126,3 @@ import getFileType from '@uppy/utils/lib/getFileType'; | ||
handleError(error) { | ||
var _error$cause; | ||
const { | ||
@@ -132,3 +133,5 @@ uppy | ||
uppy.log(error.toString()); | ||
if (error.isAuthError) { | ||
if (error.isAuthError || ((_error$cause = error.cause) == null ? void 0 : _error$cause.name) === 'AbortError') { | ||
// authError just means we're not authenticated, don't show to user | ||
// AbortError means the user has clicked "cancel" on an operation | ||
return; | ||
@@ -135,0 +138,0 @@ } |
{ | ||
"name": "@uppy/provider-views", | ||
"description": "View library for Uppy remote provider plugins.", | ||
"version": "3.4.0", | ||
"version": "3.4.1", | ||
"license": "MIT", | ||
@@ -23,3 +23,3 @@ "main": "lib/index.js", | ||
"dependencies": { | ||
"@uppy/utils": "^5.4.1", | ||
"@uppy/utils": "^5.4.2", | ||
"classnames": "^2.2.6", | ||
@@ -26,0 +26,0 @@ "nanoid": "^4.0.0", |
@@ -5,2 +5,4 @@ import { h } from 'preact' | ||
import remoteFileObjToLocal from '@uppy/utils/lib/remoteFileObjToLocal' | ||
import { useMemo } from 'preact/hooks' | ||
import VirtualList from '@uppy/utils/lib/VirtualList' | ||
import SearchFilterInput from './SearchFilterInput.jsx' | ||
@@ -12,2 +14,59 @@ import FooterActions from './FooterActions.jsx' | ||
function ListItem (props) { | ||
const { | ||
currentSelection, | ||
uppyFiles, | ||
viewType, | ||
isChecked, | ||
toggleCheckbox, | ||
recordShiftKeyPress, | ||
showTitles, | ||
i18n, | ||
validateRestrictions, | ||
getNextFolder, | ||
columns, | ||
f, | ||
} = props | ||
if (f.isFolder) { | ||
return Item({ | ||
columns, | ||
showTitles, | ||
viewType, | ||
i18n, | ||
id: f.id, | ||
title: f.name, | ||
getItemIcon: () => f.icon, | ||
isChecked: isChecked(f), | ||
toggleCheckbox: (event) => toggleCheckbox(event, f), | ||
recordShiftKeyPress, | ||
type: 'folder', | ||
isDisabled: isChecked(f)?.loading, | ||
isCheckboxDisabled: f.id === VIRTUAL_SHARED_DIR, | ||
handleFolderClick: () => getNextFolder(f), | ||
}) | ||
} | ||
const restrictionError = validateRestrictions(remoteFileObjToLocal(f), [ | ||
...uppyFiles, | ||
...currentSelection, | ||
]) | ||
return Item({ | ||
id: f.id, | ||
title: f.name, | ||
author: f.author, | ||
getItemIcon: () => f.icon, | ||
isChecked: isChecked(f), | ||
toggleCheckbox: (event) => toggleCheckbox(event, f), | ||
recordShiftKeyPress, | ||
columns, | ||
showTitles, | ||
viewType, | ||
i18n, | ||
type: 'file', | ||
isDisabled: restrictionError && !isChecked(f), | ||
restrictionError, | ||
}) | ||
} | ||
function Browser (props) { | ||
@@ -42,2 +101,3 @@ const { | ||
noResultsLabel, | ||
loadAllFiles, | ||
} = props | ||
@@ -47,2 +107,4 @@ | ||
const rows = useMemo(() => [...folders, ...files], [folders, files]) | ||
return ( | ||
@@ -92,5 +154,30 @@ <div | ||
if (!folders.length && !files.length) { | ||
return <div className="uppy-Provider-empty">{noResultsLabel}</div> | ||
} | ||
if (loadAllFiles) { | ||
return ( | ||
<div className="uppy-Provider-empty"> | ||
{noResultsLabel} | ||
<div className="uppy-ProviderBrowser-body"> | ||
<ul className="uppy-ProviderBrowser-list"> | ||
<VirtualList | ||
data={rows} | ||
renderRow={(f) => ( | ||
<ListItem | ||
currentSelection={currentSelection} | ||
uppyFiles={uppyFiles} | ||
viewType={viewType} | ||
isChecked={isChecked} | ||
toggleCheckbox={toggleCheckbox} | ||
recordShiftKeyPress={recordShiftKeyPress} | ||
showTitles={showTitles} | ||
i18n={i18n} | ||
validateRestrictions={validateRestrictions} | ||
getNextFolder={getNextFolder} | ||
columns={columns} | ||
f={f} | ||
/> | ||
)} | ||
rowHeight={31} | ||
/> | ||
</ul> | ||
</div> | ||
@@ -109,44 +196,18 @@ ) | ||
> | ||
{folders.map((folder) => { | ||
return Item({ | ||
columns, | ||
showTitles, | ||
viewType, | ||
i18n, | ||
id: folder.id, | ||
title: folder.name, | ||
getItemIcon: () => folder.icon, | ||
isChecked: isChecked(folder), | ||
toggleCheckbox: (event) => toggleCheckbox(event, folder), | ||
recordShiftKeyPress, | ||
type: 'folder', | ||
isDisabled: isChecked(folder)?.loading, | ||
isCheckboxDisabled: folder.id === VIRTUAL_SHARED_DIR, | ||
handleFolderClick: () => getNextFolder(folder), | ||
}) | ||
})} | ||
{files.map((file) => { | ||
const restrictionError = validateRestrictions( | ||
remoteFileObjToLocal(file), | ||
[...uppyFiles, ...currentSelection], | ||
) | ||
return Item({ | ||
id: file.id, | ||
title: file.name, | ||
author: file.author, | ||
getItemIcon: () => file.icon, | ||
isChecked: isChecked(file), | ||
toggleCheckbox: (event) => toggleCheckbox(event, file), | ||
recordShiftKeyPress, | ||
columns, | ||
showTitles, | ||
viewType, | ||
i18n, | ||
type: 'file', | ||
isDisabled: restrictionError && !isChecked(file), | ||
restrictionError, | ||
}) | ||
})} | ||
{rows.map((f) => ( | ||
<ListItem | ||
currentSelection={currentSelection} | ||
uppyFiles={uppyFiles} | ||
viewType={viewType} | ||
isChecked={isChecked} | ||
toggleCheckbox={toggleCheckbox} | ||
recordShiftKeyPress={recordShiftKeyPress} | ||
showTitles={showTitles} | ||
i18n={i18n} | ||
validateRestrictions={validateRestrictions} | ||
getNextFolder={getNextFolder} | ||
columns={columns} | ||
f={f} | ||
/> | ||
))} | ||
</ul> | ||
@@ -153,0 +214,0 @@ </div> |
@@ -41,5 +41,5 @@ import { h } from 'preact' | ||
const { alt } = props | ||
return <img src={itemIconString} alt={alt} loading="lazy" /> | ||
return <img src={itemIconString} alt={alt} loading="lazy" width={16} height={16} /> | ||
} | ||
} | ||
} |
@@ -98,2 +98,25 @@ import { h } from 'preact' | ||
#abortController | ||
async #withAbort (op) { | ||
// prevent multiple requests in parallel from causing race conditions | ||
this.#abortController?.abort() | ||
const abortController = new AbortController() | ||
this.#abortController = abortController | ||
const cancelRequest = () => { | ||
abortController.abort() | ||
this.clearSelection() | ||
} | ||
try { | ||
this.plugin.uppy.on('dashboard:close-panel', cancelRequest) | ||
this.plugin.uppy.on('cancel-all', cancelRequest) | ||
await op(abortController.signal) | ||
} finally { | ||
this.plugin.uppy.off('dashboard:close-panel', cancelRequest) | ||
this.plugin.uppy.off('cancel-all', cancelRequest) | ||
this.#abortController = undefined | ||
} | ||
} | ||
async #list ({ requestPath, absDirPath, signal }) { | ||
@@ -112,6 +135,6 @@ const { username, nextPagePath, items } = await this.provider.list(requestPath, { signal }) | ||
async #listFilesAndFolders ({ requestPath, breadcrumbs, signal }) { | ||
async #listFilesAndFolders ({ breadcrumbs, signal }) { | ||
const absDirPath = formatBreadcrumbs(breadcrumbs) | ||
const { items, nextPagePath } = await this.#list({ requestPath, absDirPath, signal }) | ||
const { items, nextPagePath } = await this.#list({ requestPath: this.nextPagePath, absDirPath, signal }) | ||
@@ -144,53 +167,41 @@ this.nextPagePath = nextPagePath | ||
async getFolder (requestPath, name) { | ||
const controller = new AbortController() | ||
const cancelRequest = () => { | ||
controller.abort() | ||
this.clearSelection() | ||
} | ||
this.plugin.uppy.on('dashboard:close-panel', cancelRequest) | ||
this.plugin.uppy.on('cancel-all', cancelRequest) | ||
this.setLoading(true) | ||
try { | ||
this.lastCheckbox = undefined | ||
await this.#withAbort(async (signal) => { | ||
this.lastCheckbox = undefined | ||
let { breadcrumbs } = this.plugin.getPluginState() | ||
let { breadcrumbs } = this.plugin.getPluginState() | ||
const index = breadcrumbs.findIndex((dir) => requestPath === dir.requestPath) | ||
const index = breadcrumbs.findIndex((dir) => requestPath === dir.requestPath) | ||
if (index !== -1) { | ||
// means we navigated back to a known directory (already in the stack), so cut the stack off there | ||
breadcrumbs = breadcrumbs.slice(0, index + 1) | ||
} else { | ||
// we have navigated into a new (unknown) folder, add it to the stack | ||
breadcrumbs = [...breadcrumbs, { requestPath, name }] | ||
} | ||
if (index !== -1) { | ||
// means we navigated back to a known directory (already in the stack), so cut the stack off there | ||
breadcrumbs = breadcrumbs.slice(0, index + 1) | ||
} else { | ||
// we have navigated into a new (unknown) folder, add it to the stack | ||
breadcrumbs = [...breadcrumbs, { requestPath, name }] | ||
} | ||
let files = [] | ||
let folders = [] | ||
do { | ||
const { files: newFiles, folders: newFolders } = await this.#listFilesAndFolders({ | ||
requestPath, breadcrumbs, signal: controller.signal, | ||
}) | ||
this.nextPagePath = requestPath | ||
let files = [] | ||
let folders = [] | ||
do { | ||
const { files: newFiles, folders: newFolders } = await this.#listFilesAndFolders({ | ||
breadcrumbs, signal, | ||
}) | ||
files = files.concat(newFiles) | ||
folders = folders.concat(newFolders) | ||
files = files.concat(newFiles) | ||
folders = folders.concat(newFolders) | ||
this.setLoading(this.plugin.uppy.i18n('loadedXFiles', { numFiles: files.length + folders.length })) | ||
} while ( | ||
this.opts.loadAllFiles && this.nextPagePath | ||
) | ||
this.setLoading(this.plugin.uppy.i18n('loadedXFiles', { numFiles: files.length + folders.length })) | ||
} while ( | ||
this.opts.loadAllFiles && this.nextPagePath | ||
) | ||
this.plugin.setPluginState({ folders, files, breadcrumbs, filterInput: '' }) | ||
this.plugin.setPluginState({ folders, files, breadcrumbs, filterInput: '' }) | ||
}) | ||
} catch (err) { | ||
if (err.cause?.name === 'AbortError') { | ||
// Expected, user clicked “cancel” | ||
return | ||
} | ||
this.handleError(err) | ||
} finally { | ||
this.setLoading(false) | ||
this.plugin.uppy.off('dashboard:close-panel', cancelRequest) | ||
this.plugin.uppy.off('cancel-all', cancelRequest) | ||
} | ||
@@ -212,5 +223,6 @@ } | ||
*/ | ||
logout () { | ||
this.provider.logout() | ||
.then((res) => { | ||
async logout () { | ||
try { | ||
await this.#withAbort(async (signal) => { | ||
const res = await this.provider.logout({ signal }) | ||
if (res.ok) { | ||
@@ -234,3 +246,6 @@ if (!res.revoked) { | ||
} | ||
}).catch(this.handleError) | ||
}) | ||
} catch (err) { | ||
this.handleError(err) | ||
} | ||
} | ||
@@ -289,18 +304,18 @@ | ||
async handleScroll (event) { | ||
const requestPath = this.nextPagePath || null | ||
if (this.shouldHandleScroll(event) && requestPath) { | ||
if (this.shouldHandleScroll(event) && this.nextPagePath) { | ||
this.isHandlingScroll = true | ||
try { | ||
const { files, folders, breadcrumbs } = this.plugin.getPluginState() | ||
await this.#withAbort(async (signal) => { | ||
const { files, folders, breadcrumbs } = this.plugin.getPluginState() | ||
const { files: newFiles, folders: newFolders } = await this.#listFilesAndFolders({ | ||
requestPath, breadcrumbs, | ||
}) | ||
const { files: newFiles, folders: newFolders } = await this.#listFilesAndFolders({ | ||
breadcrumbs, signal, | ||
}) | ||
const combinedFiles = files.concat(newFiles) | ||
const combinedFolders = folders.concat(newFolders) | ||
const combinedFiles = files.concat(newFiles) | ||
const combinedFolders = folders.concat(newFolders) | ||
this.plugin.setPluginState({ folders: combinedFolders, files: combinedFiles }) | ||
this.plugin.setPluginState({ folders: combinedFolders, files: combinedFiles }) | ||
}) | ||
} catch (error) { | ||
@@ -314,7 +329,7 @@ this.handleError(error) | ||
async #recursivelyListAllFiles ({ requestPath, absDirPath, relDirPath, queue, onFiles }) { | ||
async #recursivelyListAllFiles ({ requestPath, absDirPath, relDirPath, queue, onFiles, signal }) { | ||
let curPath = requestPath | ||
while (curPath) { | ||
const res = await this.#list({ requestPath: curPath, absDirPath }) | ||
const res = await this.#list({ requestPath: curPath, absDirPath, signal }) | ||
curPath = res.nextPagePath | ||
@@ -335,2 +350,3 @@ | ||
onFiles, | ||
signal, | ||
}) | ||
@@ -345,83 +361,86 @@ ))) | ||
try { | ||
const { currentSelection } = this.plugin.getPluginState() | ||
await this.#withAbort(async (signal) => { | ||
const { currentSelection } = this.plugin.getPluginState() | ||
const messages = [] | ||
const newFiles = [] | ||
const messages = [] | ||
const newFiles = [] | ||
for (const selectedItem of currentSelection) { | ||
const { requestPath } = selectedItem | ||
for (const selectedItem of currentSelection) { | ||
const { requestPath } = selectedItem | ||
const withRelDirPath = (newItem) => ({ | ||
...newItem, | ||
// calculate the file's path relative to the user's selected item's path | ||
// see https://github.com/transloadit/uppy/pull/4537#issuecomment-1614236655 | ||
relDirPath: newItem.absDirPath.replace(selectedItem.absDirPath, '').replace(/^\//, ''), | ||
}) | ||
const withRelDirPath = (newItem) => ({ | ||
...newItem, | ||
// calculate the file's path relative to the user's selected item's path | ||
// see https://github.com/transloadit/uppy/pull/4537#issuecomment-1614236655 | ||
relDirPath: newItem.absDirPath.replace(selectedItem.absDirPath, '').replace(/^\//, ''), | ||
}) | ||
if (selectedItem.isFolder) { | ||
let isEmpty = true | ||
let numNewFiles = 0 | ||
if (selectedItem.isFolder) { | ||
let isEmpty = true | ||
let numNewFiles = 0 | ||
const queue = new PQueue({ concurrency: 6 }) | ||
const queue = new PQueue({ concurrency: 6 }) | ||
const onFiles = (files) => { | ||
for (const newFile of files) { | ||
const tagFile = this.getTagFile(newFile) | ||
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(withRelDirPath(newFile)) | ||
numNewFiles++ | ||
this.setLoading(this.plugin.uppy.i18n('addedNumFiles', { numFiles: numNewFiles })) | ||
const onFiles = (files) => { | ||
for (const newFile of files) { | ||
const tagFile = this.getTagFile(newFile) | ||
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(withRelDirPath(newFile)) | ||
numNewFiles++ | ||
this.setLoading(this.plugin.uppy.i18n('addedNumFiles', { numFiles: numNewFiles })) | ||
} | ||
isEmpty = false | ||
} | ||
isEmpty = false | ||
} | ||
} | ||
await this.#recursivelyListAllFiles({ | ||
requestPath, | ||
absDirPath: prependPath(selectedItem.absDirPath, selectedItem.name), | ||
relDirPath: selectedItem.name, | ||
queue, | ||
onFiles, | ||
}) | ||
await queue.onIdle() | ||
await this.#recursivelyListAllFiles({ | ||
requestPath, | ||
absDirPath: prependPath(selectedItem.absDirPath, selectedItem.name), | ||
relDirPath: selectedItem.name, | ||
queue, | ||
onFiles, | ||
signal, | ||
}) | ||
await queue.onIdle() | ||
let message | ||
if (isEmpty) { | ||
message = this.plugin.uppy.i18n('emptyFolderAdded') | ||
} else if (numNewFiles === 0) { | ||
message = this.plugin.uppy.i18n('folderAlreadyAdded', { | ||
folder: selectedItem.name, | ||
}) | ||
let message | ||
if (isEmpty) { | ||
message = this.plugin.uppy.i18n('emptyFolderAdded') | ||
} else if (numNewFiles === 0) { | ||
message = this.plugin.uppy.i18n('folderAlreadyAdded', { | ||
folder: selectedItem.name, | ||
}) | ||
} else { | ||
// TODO we don't really know at this point whether any files were actually added | ||
// (only later after addFiles has been called) so we should probably rewrite this. | ||
// Example: If all files fail to add due to restriction error, it will still say "Added 100 files from folder" | ||
message = this.plugin.uppy.i18n('folderAdded', { | ||
smart_count: numNewFiles, folder: selectedItem.name, | ||
}) | ||
} | ||
messages.push(message) | ||
} else { | ||
// TODO we don't really know at this point whether any files were actually added | ||
// (only later after addFiles has been called) so we should probably rewrite this. | ||
// Example: If all files fail to add due to restriction error, it will still say "Added 100 files from folder" | ||
message = this.plugin.uppy.i18n('folderAdded', { | ||
smart_count: numNewFiles, folder: selectedItem.name, | ||
}) | ||
newFiles.push(withRelDirPath(selectedItem)) | ||
} | ||
messages.push(message) | ||
} else { | ||
newFiles.push(withRelDirPath(selectedItem)) | ||
} | ||
} | ||
// 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))) | ||
// 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.plugin.setPluginState({ filterInput: '' }) | ||
messages.forEach(message => this.plugin.uppy.info(message)) | ||
this.clearSelection() | ||
this.clearSelection() | ||
}) | ||
} catch (err) { | ||
@@ -467,2 +486,3 @@ this.handleError(err) | ||
getFolder: this.getFolder, | ||
loadAllFiles: this.opts.loadAllFiles, | ||
@@ -469,0 +489,0 @@ // For SearchFilterInput component |
@@ -51,3 +51,5 @@ import getFileType from '@uppy/utils/lib/getFileType' | ||
if (error.isAuthError) { | ||
if (error.isAuthError || error.cause?.name === 'AbortError') { | ||
// authError just means we're not authenticated, don't show to user | ||
// AbortError means the user has clicked "cancel" on an operation | ||
return | ||
@@ -54,0 +56,0 @@ } |
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
271520
3855
Updated@uppy/utils@^5.4.2