diff --git a/packages/@uppy/google-drive/src/GoogleDrive.jsx b/packages/@uppy/google-drive/src/GoogleDrive.jsx
index e4d6a26b17..96433c33ef 100644
--- a/packages/@uppy/google-drive/src/GoogleDrive.jsx
+++ b/packages/@uppy/google-drive/src/GoogleDrive.jsx
@@ -72,7 +72,7 @@ export default class GoogleDrive extends UIPlugin {
onFirstRender () {
return Promise.all([
this.provider.fetchPreAuthToken(),
- this.view.getFolder('root', '/'),
+ this.view.getFolder('root'),
])
}
diff --git a/packages/@uppy/provider-views/README.md b/packages/@uppy/provider-views/README.md
index e5992537a9..c3be4986bb 100644
--- a/packages/@uppy/provider-views/README.md
+++ b/packages/@uppy/provider-views/README.md
@@ -26,7 +26,7 @@ class GoogleDrive extends UIPlugin {
onFirstRender () {
return Promise.all([
this.provider.fetchPreAuthToken(),
- this.view.getFolder('root', '/'),
+ this.view.getFolder('root'),
])
}
diff --git a/packages/@uppy/provider-views/src/Breadcrumbs.jsx b/packages/@uppy/provider-views/src/Breadcrumbs.jsx
index 74bd91f3c1..b2d33ec766 100644
--- a/packages/@uppy/provider-views/src/Breadcrumbs.jsx
+++ b/packages/@uppy/provider-views/src/Breadcrumbs.jsx
@@ -18,18 +18,18 @@ const Breadcrumb = (props) => {
}
export default (props) => {
- const { getFolder, title, breadcrumbsIcon, directories } = props
+ const { getFolder, title, breadcrumbsIcon, breadcrumbs } = props
return (
{breadcrumbsIcon}
{
- directories.map((directory, i) => (
+ breadcrumbs.map((directory, i) => (
getFolder(directory.id)}
- title={i === 0 ? title : directory.title}
- isLast={i + 1 === directories.length}
+ getFolder={() => getFolder(directory.requestPath)}
+ title={i === 0 ? title : directory.name}
+ isLast={i + 1 === breadcrumbs.length}
/>
))
}
diff --git a/packages/@uppy/provider-views/src/ProviderView/Header.jsx b/packages/@uppy/provider-views/src/ProviderView/Header.jsx
index eeb36417f2..1683ff4860 100644
--- a/packages/@uppy/provider-views/src/ProviderView/Header.jsx
+++ b/packages/@uppy/provider-views/src/ProviderView/Header.jsx
@@ -6,7 +6,7 @@ export default (props) => {
if (props.showBreadcrumbs) {
components.push(Breadcrumbs({
getFolder: props.getFolder,
- directories: props.directories,
+ breadcrumbs: props.breadcrumbs,
breadcrumbsIcon: props.pluginIcon && props.pluginIcon(),
title: props.title,
}))
diff --git a/packages/@uppy/provider-views/src/ProviderView/ProviderView.jsx b/packages/@uppy/provider-views/src/ProviderView/ProviderView.jsx
index 756d8dae28..a766184acc 100644
--- a/packages/@uppy/provider-views/src/ProviderView/ProviderView.jsx
+++ b/packages/@uppy/provider-views/src/ProviderView/ProviderView.jsx
@@ -32,6 +32,15 @@ function isOriginAllowed (origin, allowedOrigin) {
.some((pattern) => pattern?.test(origin) || pattern?.test(`${origin}/`)) // allowing for trailing '/'
}
+function formatBreadcrumbs (breadcrumbs) {
+ return breadcrumbs.slice(1).map((directory) => directory.name).join('/')
+}
+
+function prependPath (path, component) {
+ if (!path) return component
+ return `${path}/${component}`
+}
+
/**
* Class to easily generate generic views for Provider plugins
*/
@@ -74,7 +83,7 @@ export default class ProviderView extends View {
authenticated: false,
files: [],
folders: [],
- directories: [],
+ breadcrumbs: [],
filterInput: '',
isSearchVisible: false,
currentSelection: [],
@@ -86,9 +95,30 @@ export default class ProviderView extends View {
// Nothing.
}
- #updateFilesAndFolders (res, files, folders) {
- this.nextPagePath = res.nextPagePath
- res.items.forEach((item) => {
+ async #list ({ requestPath, absDirPath, signal }) {
+ const { username, nextPagePath, items } = await this.provider.list(requestPath, { signal })
+ this.username = username || this.username
+
+ return {
+ items: items.map((item) => ({
+ ...item,
+ absDirPath,
+ })),
+ nextPagePath,
+ }
+ }
+
+ async #listFilesAndFolders ({ requestPath, breadcrumbs, signal }) {
+ const absDirPath = formatBreadcrumbs(breadcrumbs)
+
+ const { items, nextPagePath } = await this.#list({ requestPath, absDirPath, signal })
+
+ this.nextPagePath = nextPagePath
+
+ const files = []
+ const folders = []
+
+ items.forEach((item) => {
if (item.isFolder) {
folders.push(item)
} else {
@@ -96,59 +126,60 @@ export default class ProviderView extends View {
}
})
- this.plugin.setPluginState({ folders, files })
+ return { files, folders }
}
/**
- * Based on folder ID, fetch a new folder and update it to state
+ * Select a folder based on its id: fetches the folder and then updates state with its contents
+ * TODO rename to something better like selectFolder or navigateToFolder (breaking change?)
*
- * @param {string} id Folder id
+ * @param {string} requestPath
+ * the path we need to use when sending list request to companion (for some providers it's different from ID)
+ * @param {string} name used in the UI and to build the absDirPath
* @returns {Promise} Folders/files in folder
*/
- async getFolder (id, name) {
+ async getFolder (requestPath, name) {
const controller = new AbortController()
const cancelRequest = () => {
controller.abort()
this.clearSelection()
}
- const getNewBreadcrumbsDirectories = () => {
- const state = this.plugin.getPluginState()
- const index = state.directories.findIndex((dir) => id === dir.id)
-
- if (index !== -1) {
- return state.directories.slice(0, index + 1)
- }
- return state.directories.concat([{ id, title: name }])
- }
this.plugin.uppy.on('dashboard:close-panel', cancelRequest)
this.plugin.uppy.on('cancel-all', cancelRequest)
- this.setLoading(true)
+ this.setLoading(true)
try {
- const folders = []
- const files = []
- this.nextPagePath = id
+ 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 res = await this.provider.list(this.nextPagePath, { signal: controller.signal })
+ const { files: newFiles, folders: newFolders } = await this.#listFilesAndFolders({
+ requestPath, breadcrumbs, signal: controller.signal,
+ })
- for (const f of res.items) {
- if (f.isFolder) folders.push(f)
- else files.push(f)
- }
+ files = files.concat(newFiles)
+ folders = folders.concat(newFolders)
- this.nextPagePath = res.nextPagePath
- if (res.username) this.username = res.username
this.setLoading(this.plugin.uppy.i18n('loadedXFiles', { numFiles: files.length + folders.length }))
} while (
- this.nextPagePath && this.opts.loadAllFiles
+ this.opts.loadAllFiles && this.nextPagePath
)
- const directories = getNewBreadcrumbsDirectories(this.nextPagePath)
-
- this.plugin.setPluginState({ files, folders, directories, filterInput: '' })
- this.lastCheckbox = undefined
+ this.plugin.setPluginState({ folders, files, breadcrumbs, filterInput: '' })
} catch (err) {
if (err.cause?.name === 'AbortError') {
// Expected, user clicked “cancel”
@@ -191,7 +222,7 @@ export default class ProviderView extends View {
authenticated: false,
files: [],
folders: [],
- directories: [],
+ breadcrumbs: [],
filterInput: '',
}
this.plugin.setPluginState(newState)
@@ -250,16 +281,22 @@ export default class ProviderView extends View {
}
async handleScroll (event) {
- const path = this.nextPagePath || null
+ const requestPath = this.nextPagePath || null
- if (this.shouldHandleScroll(event) && path) {
+ if (this.shouldHandleScroll(event) && requestPath) {
this.isHandlingScroll = true
try {
- const response = await this.provider.list(path)
- const { files, folders } = this.plugin.getPluginState()
+ const { files, folders, breadcrumbs } = this.plugin.getPluginState()
- this.#updateFilesAndFolders(response, files, folders)
+ const { files: newFiles, folders: newFolders } = await this.#listFilesAndFolders({
+ requestPath, breadcrumbs,
+ })
+
+ const combinedFiles = files.concat(newFiles)
+ const combinedFolders = folders.concat(newFolders)
+
+ this.plugin.setPluginState({ folders: combinedFolders, files: combinedFiles })
} catch (error) {
this.handleError(error)
} finally {
@@ -268,11 +305,11 @@ export default class ProviderView extends View {
}
}
- async recursivelyListAllFiles (path, queue, onFiles) {
- let curPath = path
+ async #recursivelyListAllFiles ({ requestPath, absDirPath, relDirPath, queue, onFiles }) {
+ let curPath = requestPath
while (curPath) {
- const res = await this.provider.list(curPath)
+ const res = await this.#list({ requestPath: curPath, absDirPath })
curPath = res.nextPagePath
const files = res.items.filter((item) => !item.isFolder)
@@ -282,7 +319,13 @@ export default class ProviderView extends View {
// recursively queue call to self for each folder
const promises = folders.map(async (folder) => queue.add(async () => (
- this.recursivelyListAllFiles(folder.requestPath, queue, onFiles)
+ this.#recursivelyListAllFiles({
+ requestPath: folder.requestPath,
+ absDirPath: prependPath(absDirPath, folder.name),
+ relDirPath: prependPath(relDirPath, folder.name),
+ queue,
+ onFiles,
+ })
)))
await Promise.all(promises) // in case we get an error
}
@@ -296,9 +339,17 @@ export default class ProviderView extends View {
const messages = []
const newFiles = []
- for (const file of currentSelection) {
- if (file.isFolder) {
- const { requestPath, name } = file
+ 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(/^\//, ''),
+ })
+
+ if (selectedItem.isFolder) {
let isEmpty = true
let numNewFiles = 0
@@ -313,7 +364,7 @@ export default class ProviderView extends View {
// 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(newFile)
+ newFiles.push(withRelDirPath(newFile))
numNewFiles++
this.setLoading(this.plugin.uppy.i18n('addedNumFiles', { numFiles: numNewFiles }))
}
@@ -321,7 +372,13 @@ export default class ProviderView extends View {
}
}
- await this.recursivelyListAllFiles(requestPath, queue, onFiles)
+ await this.#recursivelyListAllFiles({
+ requestPath,
+ absDirPath: prependPath(selectedItem.absDirPath, selectedItem.name),
+ relDirPath: selectedItem.name,
+ queue,
+ onFiles,
+ })
await queue.onIdle()
let message
@@ -329,20 +386,20 @@ export default class ProviderView extends View {
message = this.plugin.uppy.i18n('emptyFolderAdded')
} else if (numNewFiles === 0) {
message = this.plugin.uppy.i18n('folderAlreadyAdded', {
- folder: name,
+ 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: name,
+ smart_count: numNewFiles, folder: selectedItem.name,
})
}
messages.push(message)
} else {
- newFiles.push(file)
+ newFiles.push(withRelDirPath(selectedItem))
}
}
@@ -380,7 +437,7 @@ export default class ProviderView extends View {
const headerProps = {
showBreadcrumbs: targetViewOptions.showBreadcrumbs,
getFolder: this.getFolder,
- directories: this.plugin.getPluginState().directories,
+ breadcrumbs: this.plugin.getPluginState().breadcrumbs,
pluginIcon: this.plugin.icon,
title: this.plugin.title,
logout: this.logout,
diff --git a/packages/@uppy/provider-views/src/SearchProviderView/SearchProviderView.jsx b/packages/@uppy/provider-views/src/SearchProviderView/SearchProviderView.jsx
index f77e98477e..48a64a3605 100644
--- a/packages/@uppy/provider-views/src/SearchProviderView/SearchProviderView.jsx
+++ b/packages/@uppy/provider-views/src/SearchProviderView/SearchProviderView.jsx
@@ -46,7 +46,7 @@ export default class SearchProviderView extends View {
isInputMode: true,
files: [],
folders: [],
- directories: [],
+ breadcrumbs: [],
filterInput: '',
currentSelection: [],
searchTerm: null,
diff --git a/packages/@uppy/provider-views/src/View.js b/packages/@uppy/provider-views/src/View.js
index 48e5be6913..81fffef3d9 100644
--- a/packages/@uppy/provider-views/src/View.js
+++ b/packages/@uppy/provider-views/src/View.js
@@ -92,6 +92,11 @@ export default class View {
if (file.author.url) tagFile.meta.authorUrl = file.author.url
}
+ // add relativePath similar to non-remote files: https://github.com/transloadit/uppy/pull/4486#issuecomment-1579203717
+ if (file.relDirPath != null) tagFile.meta.relativePath = file.relDirPath ? `${file.relDirPath}/${tagFile.name}` : null
+ // and absolutePath (with leading slash) https://github.com/transloadit/uppy/pull/4537#issuecomment-1614236655
+ if (file.absDirPath != null) tagFile.meta.absolutePath = file.absDirPath ? `/${file.absDirPath}/${tagFile.name}` : `/${tagFile.name}`
+
return tagFile
}
diff --git a/packages/@uppy/utils/src/getDroppedFiles/utils/webkitGetAsEntryApi/index.js b/packages/@uppy/utils/src/getDroppedFiles/utils/webkitGetAsEntryApi/index.js
index 08ee4fd3b8..c84d62e39d 100644
--- a/packages/@uppy/utils/src/getDroppedFiles/utils/webkitGetAsEntryApi/index.js
+++ b/packages/@uppy/utils/src/getDroppedFiles/utils/webkitGetAsEntryApi/index.js
@@ -27,17 +27,19 @@ function getAsFileSystemHandleFromEntry (entry, logDropError) {
}
async function* createPromiseToAddFileOrParseDirectory (entry, relativePath, lastResortFile = undefined) {
+ const getNextRelativePath = () => `${relativePath}/${entry.name}`
+
// For each dropped item, - make sure it's a file/directory, and start deepening in!
if (entry.kind === 'file') {
const file = await entry.getFile()
if (file != null) {
- file.relativePath = relativePath ? `${relativePath}/${entry.name}` : null
+ file.relativePath = relativePath ? getNextRelativePath() : null
yield file
} else if (lastResortFile != null) yield lastResortFile
} else if (entry.kind === 'directory') {
for await (const handle of entry.values()) {
// Recurse on the directory, appending the dir name to the relative path
- yield* createPromiseToAddFileOrParseDirectory(handle, `${relativePath}/${entry.name}`)
+ yield* createPromiseToAddFileOrParseDirectory(handle, relativePath ? getNextRelativePath() : entry.name)
}
} else if (lastResortFile != null) yield lastResortFile
}