From b2340457c69085e0049302b495c446ea8c490c88 Mon Sep 17 00:00:00 2001 From: Evgenia Karunus Date: Tue, 12 Mar 2024 11:40:50 +0400 Subject: [PATCH 001/170] ProviderView.tsx - fix onedrive breadcrumbs --- packages/@uppy/core/src/Uppy.ts | 2 +- packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/@uppy/core/src/Uppy.ts b/packages/@uppy/core/src/Uppy.ts index 9a6d70c084..cea4386535 100644 --- a/packages/@uppy/core/src/Uppy.ts +++ b/packages/@uppy/core/src/Uppy.ts @@ -64,7 +64,7 @@ export type UnknownPlugin< export type UnknownProviderPluginState = { authenticated: boolean | undefined breadcrumbs: { - requestPath: string + requestPath?: string name?: string id?: string }[] diff --git a/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx b/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx index 72027716d3..5c6597916b 100644 --- a/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx +++ b/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx @@ -233,7 +233,7 @@ export default class ProviderView extends View< 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 if (requestPath) { + } else { // we have navigated into a new (unknown) folder, add it to the stack breadcrumbs = [...breadcrumbs, { requestPath, name }] } From 10cbbf7c8abcf82bba6b03e045c2e71fd2dc6585 Mon Sep 17 00:00:00 2001 From: Evgenia Karunus Date: Tue, 12 Mar 2024 12:34:48 +0400 Subject: [PATCH 002/170] providers - correct ch-unch-indeterminate states --- packages/@uppy/provider-views/src/Browser.tsx | 4 +- .../src/Item/components/ListLi.tsx | 25 ++-- .../src/ProviderView/ProviderView.tsx | 69 +++++++++-- packages/@uppy/provider-views/src/View.ts | 112 +++++++++--------- .../uppy-ProviderBrowser-viewType--list.scss | 2 +- .../uppy-ProviderBrowserItem-checkbox.scss | 36 ++++-- private/dev/Dashboard.js | 37 +++--- 7 files changed, 175 insertions(+), 110 deletions(-) diff --git a/packages/@uppy/provider-views/src/Browser.tsx b/packages/@uppy/provider-views/src/Browser.tsx index 006b322275..731f0eb186 100644 --- a/packages/@uppy/provider-views/src/Browser.tsx +++ b/packages/@uppy/provider-views/src/Browser.tsx @@ -56,7 +56,7 @@ function ListItem( id: f.id, title: f.name, getItemIcon: () => f.icon, - isChecked: isChecked(f), + status: isChecked(f), toggleCheckbox: (event: Event) => toggleCheckbox(event, f), recordShiftKeyPress, type: 'folder', @@ -77,9 +77,9 @@ function ListItem( title: f.name, author: f.author, getItemIcon: () => f.icon, - isChecked: isChecked(f), toggleCheckbox: (event: Event) => toggleCheckbox(event, f), isCheckboxDisabled: false, + status: isChecked(f), recordShiftKeyPress, showTitles, viewType, diff --git a/packages/@uppy/provider-views/src/Item/components/ListLi.tsx b/packages/@uppy/provider-views/src/Item/components/ListLi.tsx index c07d68309a..2b69edaeb2 100644 --- a/packages/@uppy/provider-views/src/Item/components/ListLi.tsx +++ b/packages/@uppy/provider-views/src/Item/components/ListLi.tsx @@ -15,7 +15,7 @@ type ListItemProps = { isDisabled: boolean restrictionError?: RestrictionError | null isCheckboxDisabled: boolean - isChecked: boolean + status: string toggleCheckbox: (event: Event) => void recordShiftKeyPress: (event: KeyboardEvent | MouseEvent) => void type: string @@ -35,7 +35,7 @@ export default function ListItem( isDisabled, restrictionError, isCheckboxDisabled, - isChecked, + status, toggleCheckbox, recordShiftKeyPress, type, @@ -47,6 +47,16 @@ export default function ListItem( i18n, } = props + + let statusClassName + if (status === "checked") { + statusClassName = "uppy-ProviderBrowserItem-checkbox--is-checked" + } else if (status === "unchecked") { + statusClassName = "" + } else if (status === "partial") { + statusClassName = "uppy-ProviderBrowserItem-checkbox--is-partial" + } + return (
  • ( {!isCheckboxDisabled ? name="listitem" id={id} - checked={isChecked} - aria-label={ - type === 'file' ? null : ( - i18n('allFilesFromFolderNamed', { name: title }) - ) - } + checked={status === "checked" ? true : false} + indeterminate={status === "indeterminate" ? true : false} + aria-label={type === 'file' ? null : i18n('allFilesFromFolderNamed', { name: title })} disabled={isDisabled} data-uppy-super-focusable /> diff --git a/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx b/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx index 5c6597916b..eb58f3c412 100644 --- a/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx +++ b/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx @@ -251,20 +251,67 @@ export default class ProviderView extends View< files = files.concat(newFiles) folders = folders.concat(newFolders) - this.setLoading( - this.plugin.uppy.i18n('loadedXFiles', { - numFiles: files.length + folders.length, - }), - ) + 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: '', - }) + + + + + + + + + + + const { partialTree } = this.plugin.getPluginState() + + if (!partialTree) { + const newPartialTree = [ + { requestPath: "root", cached: true }, + ...folders.map((folder) => ({ ...folder, status: "unchecked", parentId: "root", cached: false })), + ...files.map((file) => ({ ...file, status: "unchecked", parentId: "root" })), + ] + + this.plugin.setPluginState({ partialTree: newPartialTree }) + } else { + const clickedFolder = partialTree.find((folder) => folder.requestPath === (requestPath || "root")) + + // If selected folder is already filled in, don't refill it (because that would make it lose deep state!) + // Otherwise, cache the current folder! + if (!clickedFolder.cached) { + const clickedFolderContents = [ + ...folders.map((folder) => ({ ...folder, status: clickedFolder.status, parentId: clickedFolder.requestPath, cached: false })), + ...files.map((file) => ({ ...file, status: clickedFolder.status, parentId: clickedFolder.requestPath })), + ] + + // just doing `clickedFolder.cached = true` in a non-mutating way + const updatedClickedFolder = { ...clickedFolder, cached: true } + const partialTreeWithUpdatedClickedFolder = partialTree.map((folder) => + folder.requestPath === updatedClickedFolder.requestPath ? + updatedClickedFolder : + folder + ) + + this.plugin.setPluginState({ partialTree: [ + ...partialTreeWithUpdatedClickedFolder, + ...clickedFolderContents] }) + } + } + + this.plugin.setPluginState({ folders, files, breadcrumbs, filterInput: '' }) }) + + + + + + + + + + + } catch (err) { // This is the first call that happens when the provider view loads, after auth, so it's probably nice to show any // error occurring here to the user. diff --git a/packages/@uppy/provider-views/src/View.ts b/packages/@uppy/provider-views/src/View.ts index 126a46d412..0e8aae691a 100644 --- a/packages/@uppy/provider-views/src/View.ts +++ b/packages/@uppy/provider-views/src/View.ts @@ -199,71 +199,69 @@ export default class View< * toggle multiple checkboxes at once, which is done by getting all files * in between last checked file and current one. */ - toggleCheckbox = (e: Event, file: CompanionFile): void => { + toggleCheckbox = (e, fileOrFolder) => { e.stopPropagation() e.preventDefault() - ;(e.currentTarget as HTMLInputElement).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. - if (this.lastCheckbox && this.isShiftKeyPressed) { - const { currentSelection } = this.plugin.getPluginState() - const prevIndex = items.indexOf(this.lastCheckbox) - const currentIndex = items.indexOf(file) - const newSelection = - prevIndex < currentIndex ? - items.slice(prevIndex, currentIndex + 1) - : items.slice(currentIndex, prevIndex + 1) - const reducedNewSelection: CompanionFile[] = [] - - // Check restrictions on each file in currentSelection, - // reduce it to only contain files that pass restrictions - for (const item of newSelection) { - const { uppy } = this.plugin - const restrictionError = uppy.validateRestrictions( - remoteFileObjToLocal(item), - [...uppy.getFiles(), ...reducedNewSelection], - ) - - if (!restrictionError) { - reducedNewSelection.push(item) - } else { - uppy.info( - { message: restrictionError.message }, - 'error', - uppy.opts.infoTimeout, - ) - } - } - this.plugin.setPluginState({ - currentSelection: [ - ...new Set([...currentSelection, ...reducedNewSelection]), - ], + e.currentTarget.focus() + + const { partialTree } = this.plugin.getPluginState() + const newPartialTree = JSON.parse(JSON.stringify(partialTree)) + + const ourItem = newPartialTree.find((item) => item.requestPath === fileOrFolder.requestPath) + const newStatus = ourItem.status === "checked" ? "unchecked" : "checked" + ourItem.status = newStatus + + // if newStatus is "checked" - percolate down "checked" + // if newStatus is "unchecked" - percolate down "unchecked" + const percolateDown = (currentFile, status) => { + const children = newPartialTree.filter((item) => item.parentId === currentFile.requestPath) + children.forEach((item) => { + item.status = status + percolateDown(item, status) }) - 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]), - }) + percolateDown(ourItem, newStatus) + + // we do something to all of its parents. + const percolateUp = (currentFile) => { + const parentFolder = newPartialTree.find((item) => item.requestPath === currentFile.parentId) + + if (!parentFolder) return + + const parentsChildren = newPartialTree.filter((item) => item.parentId === parentFolder.requestPath) + const areAllChildrenChecked = parentsChildren.every((item) => item.status === "checked") + const areAllChildrenUnchecked = parentsChildren.every((item) => item.status === "unchecked") + + if (areAllChildrenChecked) { + parentFolder.status = "checked" + } else if (areAllChildrenUnchecked) { + parentFolder.status = "unchecked" + } else { + parentFolder.status = "partial" + } + + percolateUp(parentFolder) } + + percolateUp(ourItem) + + this.plugin.setPluginState({ partialTree: newPartialTree }) } - isChecked = (file: CompanionFile): boolean => { - 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) + isChecked = (file) => { + // Ohhh so it's actually called here...... + + const { partialTree } = this.plugin.getPluginState() + const ourFile = partialTree.find((item) => item.requestPath === file.requestPath) + // console.log({ourFile}); + + return ourFile.status + + // 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: boolean | string): void { diff --git a/packages/@uppy/provider-views/src/style/uppy-ProviderBrowser-viewType--list.scss b/packages/@uppy/provider-views/src/style/uppy-ProviderBrowser-viewType--list.scss index 29d12d67a3..afd3c329dc 100644 --- a/packages/@uppy/provider-views/src/style/uppy-ProviderBrowser-viewType--list.scss +++ b/packages/@uppy/provider-views/src/style/uppy-ProviderBrowser-viewType--list.scss @@ -55,7 +55,7 @@ } } // Checked: color the background, show the checkmark - .uppy-ProviderBrowserItem-checkbox--is-checked { + .uppy-ProviderBrowserItem-checkbox--is-checked, .uppy-ProviderBrowserItem-checkbox--is-partial { background-color: $blue; border-color: $blue; diff --git a/packages/@uppy/provider-views/src/style/uppy-ProviderBrowserItem-checkbox.scss b/packages/@uppy/provider-views/src/style/uppy-ProviderBrowserItem-checkbox.scss index 39e9840ef0..a8592182a3 100644 --- a/packages/@uppy/provider-views/src/style/uppy-ProviderBrowserItem-checkbox.scss +++ b/packages/@uppy/provider-views/src/style/uppy-ProviderBrowserItem-checkbox.scss @@ -7,18 +7,6 @@ cursor: default; } - // Checkmark icon - &::after { - position: absolute; - border-bottom: 2px solid $gray-200; - // Not using border-inline-start here: this is part of a CSS trick to get - // a check mark icon that only works in one direction - border-left: 2px solid $gray-200; - transform: rotate(-45deg); - cursor: pointer; - content: ''; - } - &:disabled::after { cursor: default; } @@ -33,4 +21,28 @@ [data-uppy-theme='dark'] & { background-color: $gray-800; } + // Checkmark icon + &::after { + position: absolute; + border-bottom: 2px solid $gray-200; + // Not using border-inline-start here: this is part of a CSS trick to get + // a check mark icon that only works in one direction + border-left: 2px solid $gray-200; + transform: rotate(-45deg); + cursor: pointer; + content: ''; + } +} + +.uppy-ProviderBrowserItem-checkbox--is-partial{ + &::after { + content: '' !important; + position: absolute !important; + top: 50% !important; + left: 20% !important; + right: 20% !important; + height: 2px !important; + background-color: $gray-200 !important; + transform: translateY(-50%) !important; + } } diff --git a/private/dev/Dashboard.js b/private/dev/Dashboard.js index 6b20714a98..aa5327cb82 100644 --- a/private/dev/Dashboard.js +++ b/private/dev/Dashboard.js @@ -2,12 +2,13 @@ /* eslint-disable import/no-extraneous-dependencies */ import Uppy, { debugLogger } from '@uppy/core' import Dashboard from '@uppy/dashboard' -import RemoteSources from '@uppy/remote-sources' +// import RemoteSources from '@uppy/remote-sources' import Webcam from '@uppy/webcam' import ScreenCapture from '@uppy/screen-capture' import GoldenRetriever from '@uppy/golden-retriever' import Tus from '@uppy/tus' import AwsS3 from '@uppy/aws-s3' +import OneDrive from '@uppy/onedrive'; import AwsS3Multipart from '@uppy/aws-s3-multipart' import XHRUpload from '@uppy/xhr-upload' import Transloadit from '@uppy/transloadit' @@ -102,30 +103,30 @@ export default () => { proudlyDisplayPoweredByUppy: true, note: `${JSON.stringify(restrictions)}`, }) - .use(GoogleDrive, { target: Dashboard, companionUrl: COMPANION_URL, companionAllowedHosts, ...getCompanionKeysParams('GOOGLE_DRIVE') }) + // .use(GoogleDrive, { target: Dashboard, companionUrl: COMPANION_URL, companionAllowedHosts, ...getCompanionKeysParams('GOOGLE_DRIVE') }) // .use(Instagram, { target: Dashboard, companionUrl: COMPANION_URL, companionAllowedHosts }) // .use(Dropbox, { target: Dashboard, companionUrl: COMPANION_URL, companionAllowedHosts }) // .use(Box, { target: Dashboard, companionUrl: COMPANION_URL, companionAllowedHosts }) // .use(Facebook, { target: Dashboard, companionUrl: COMPANION_URL, companionAllowedHosts }) - // .use(OneDrive, { target: Dashboard, companionUrl: COMPANION_URL, companionAllowedHosts }) + .use(OneDrive, { target: Dashboard, companionUrl: COMPANION_URL, companionAllowedHosts }) // .use(Zoom, { target: Dashboard, companionUrl: COMPANION_URL, companionAllowedHosts }) // .use(Url, { target: Dashboard, companionUrl: COMPANION_URL, companionAllowedHosts }) // .use(Unsplash, { target: Dashboard, companionUrl: COMPANION_URL, companionAllowedHosts }) - .use(RemoteSources, { - companionUrl: COMPANION_URL, - sources: ['Box', 'Dropbox', 'Facebook', 'Instagram', 'OneDrive', 'Unsplash', 'Zoom', 'Url'], - companionAllowedHosts, - }) - .use(Webcam, { - target: Dashboard, - showVideoSourceDropdown: true, - showRecordingLength: true, - }) - .use(Audio, { - target: Dashboard, - showRecordingLength: true, - }) - .use(ScreenCapture, { target: Dashboard }) + // .use(RemoteSources, { + // companionUrl: COMPANION_URL, + // sources: ['Box', 'Dropbox', 'Facebook', 'Instagram', 'OneDrive', 'Unsplash', 'Zoom', 'Url'], + // companionAllowedHosts, + // }) + // .use(Webcam, { + // target: Dashboard, + // showVideoSourceDropdown: true, + // showRecordingLength: true, + // }) + // .use(Audio, { + // target: Dashboard, + // showRecordingLength: true, + // }) + // .use(ScreenCapture, { target: Dashboard }) .use(Form, { target: '#upload-form' }) .use(ImageEditor, { target: Dashboard }) .use(DropTarget, { From 8796c357e654db4baeb622ac8e442b24a44510ab Mon Sep 17 00:00:00 2001 From: Evgenia Karunus Date: Tue, 12 Mar 2024 13:26:48 +0400 Subject: [PATCH 003/170] providers - made .breadcrumbs derived from .partialTree --- .../src/ProviderView/ProviderView.tsx | 56 +++++++++---------- private/dev/Dashboard.js | 37 ++++++------ 2 files changed, 45 insertions(+), 48 deletions(-) diff --git a/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx b/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx index eb58f3c412..ac4b5882a0 100644 --- a/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx +++ b/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx @@ -114,7 +114,8 @@ export default class ProviderView extends View< authenticated: undefined, // we don't know yet files: [], folders: [], - breadcrumbs: [], + partialTree: null, + currentRequestPath: null, filterInput: '', isSearchVisible: false, currentSelection: [], @@ -224,29 +225,13 @@ export default class ProviderView extends View< await this.#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: CompanionFile[] = [] let folders: CompanionFile[] = [] do { - const { files: newFiles, folders: newFolders } = - await this.#listFilesAndFolders({ - breadcrumbs, - signal, - }) + const { files: newFiles, folders: newFolders } = await this.#listFilesAndFolders({ + breadcrumbs: this.getBreadcrumbs(), signal, + }) files = files.concat(newFiles) folders = folders.concat(newFolders) @@ -299,7 +284,7 @@ export default class ProviderView extends View< } } - this.plugin.setPluginState({ folders, files, breadcrumbs, filterInput: '' }) + this.plugin.setPluginState({ folders, files, currentRequestPath: requestPath, filterInput: '' }) }) @@ -363,9 +348,10 @@ export default class ProviderView extends View< const newState = { authenticated: false, + currentRequestPath: null, + partialTree: null, files: [], folders: [], - breadcrumbs: [], filterInput: '', } this.plugin.setPluginState(newState) @@ -414,13 +400,11 @@ export default class ProviderView extends View< try { await this.#withAbort(async (signal) => { - const { files, folders, breadcrumbs } = this.plugin.getPluginState() + const { files, folders } = this.plugin.getPluginState() - const { files: newFiles, folders: newFolders } = - await this.#listFilesAndFolders({ - breadcrumbs, - signal, - }) + const { files: newFiles, folders: newFolders } = await this.#listFilesAndFolders({ + breadcrumbs: this.getBreadcrumbs(), signal, + }) const combinedFiles = files.concat(newFiles) const combinedFolders = folders.concat(newFolders) @@ -591,6 +575,20 @@ export default class ProviderView extends View< } } + getBreadcrumbs = () => { + const { partialTree, currentRequestPath } = this.plugin.getPluginState() + const breadcrumbs = [] + if (partialTree && currentRequestPath) { + const currentFolder = partialTree.find((folder) => folder.requestPath === currentRequestPath) + let parent = currentFolder + while (parent) { + breadcrumbs.push(parent) + parent = partialTree.find((folder) => folder.requestPath === parent.parentId) + } + } + return breadcrumbs.toReversed() + } + render( state: unknown, viewOptions: Omit, 'provider'> = {}, @@ -612,7 +610,7 @@ export default class ProviderView extends View< const headerProps = { showBreadcrumbs: targetViewOptions.showBreadcrumbs, getFolder: this.getFolder, - breadcrumbs: this.plugin.getPluginState().breadcrumbs, + breadcrumbs: this.getBreadcrumbs(), pluginIcon, title: this.plugin.title, logout: this.logout, diff --git a/private/dev/Dashboard.js b/private/dev/Dashboard.js index aa5327cb82..6b20714a98 100644 --- a/private/dev/Dashboard.js +++ b/private/dev/Dashboard.js @@ -2,13 +2,12 @@ /* eslint-disable import/no-extraneous-dependencies */ import Uppy, { debugLogger } from '@uppy/core' import Dashboard from '@uppy/dashboard' -// import RemoteSources from '@uppy/remote-sources' +import RemoteSources from '@uppy/remote-sources' import Webcam from '@uppy/webcam' import ScreenCapture from '@uppy/screen-capture' import GoldenRetriever from '@uppy/golden-retriever' import Tus from '@uppy/tus' import AwsS3 from '@uppy/aws-s3' -import OneDrive from '@uppy/onedrive'; import AwsS3Multipart from '@uppy/aws-s3-multipart' import XHRUpload from '@uppy/xhr-upload' import Transloadit from '@uppy/transloadit' @@ -103,30 +102,30 @@ export default () => { proudlyDisplayPoweredByUppy: true, note: `${JSON.stringify(restrictions)}`, }) - // .use(GoogleDrive, { target: Dashboard, companionUrl: COMPANION_URL, companionAllowedHosts, ...getCompanionKeysParams('GOOGLE_DRIVE') }) + .use(GoogleDrive, { target: Dashboard, companionUrl: COMPANION_URL, companionAllowedHosts, ...getCompanionKeysParams('GOOGLE_DRIVE') }) // .use(Instagram, { target: Dashboard, companionUrl: COMPANION_URL, companionAllowedHosts }) // .use(Dropbox, { target: Dashboard, companionUrl: COMPANION_URL, companionAllowedHosts }) // .use(Box, { target: Dashboard, companionUrl: COMPANION_URL, companionAllowedHosts }) // .use(Facebook, { target: Dashboard, companionUrl: COMPANION_URL, companionAllowedHosts }) - .use(OneDrive, { target: Dashboard, companionUrl: COMPANION_URL, companionAllowedHosts }) + // .use(OneDrive, { target: Dashboard, companionUrl: COMPANION_URL, companionAllowedHosts }) // .use(Zoom, { target: Dashboard, companionUrl: COMPANION_URL, companionAllowedHosts }) // .use(Url, { target: Dashboard, companionUrl: COMPANION_URL, companionAllowedHosts }) // .use(Unsplash, { target: Dashboard, companionUrl: COMPANION_URL, companionAllowedHosts }) - // .use(RemoteSources, { - // companionUrl: COMPANION_URL, - // sources: ['Box', 'Dropbox', 'Facebook', 'Instagram', 'OneDrive', 'Unsplash', 'Zoom', 'Url'], - // companionAllowedHosts, - // }) - // .use(Webcam, { - // target: Dashboard, - // showVideoSourceDropdown: true, - // showRecordingLength: true, - // }) - // .use(Audio, { - // target: Dashboard, - // showRecordingLength: true, - // }) - // .use(ScreenCapture, { target: Dashboard }) + .use(RemoteSources, { + companionUrl: COMPANION_URL, + sources: ['Box', 'Dropbox', 'Facebook', 'Instagram', 'OneDrive', 'Unsplash', 'Zoom', 'Url'], + companionAllowedHosts, + }) + .use(Webcam, { + target: Dashboard, + showVideoSourceDropdown: true, + showRecordingLength: true, + }) + .use(Audio, { + target: Dashboard, + showRecordingLength: true, + }) + .use(ScreenCapture, { target: Dashboard }) .use(Form, { target: '#upload-form' }) .use(ImageEditor, { target: Dashboard }) .use(DropTarget, { From e2d0d043165c8523d3a21c3a5ba6d3ba38e6c7a8 Mon Sep 17 00:00:00 2001 From: Evgenia Karunus Date: Thu, 28 Mar 2024 02:08:00 +0400 Subject: [PATCH 004/170] everywhere - { files, folders, isChecked } => .partialTree GoogleDrive - travelling down into folders works - checking a file works - breadcrumbs DONT work --- .../server/provider/instagram/graph/index.js | 2 +- packages/@uppy/core/src/Uppy.ts | 22 ++- .../google-drive/src/DriveProviderViews.ts | 18 +-- packages/@uppy/provider-views/src/Browser.tsx | 55 +++---- .../src/Item/components/GridLi.tsx | 18 ++- .../src/Item/components/ListLi.tsx | 4 +- .../@uppy/provider-views/src/Item/index.tsx | 7 +- .../src/ProviderView/ProviderView.tsx | 147 ++++++++++-------- packages/@uppy/provider-views/src/View.ts | 46 ++---- 9 files changed, 172 insertions(+), 147 deletions(-) diff --git a/packages/@uppy/companion/src/server/provider/instagram/graph/index.js b/packages/@uppy/companion/src/server/provider/instagram/graph/index.js index 4bf05c3e0f..3e4fd9d3c2 100644 --- a/packages/@uppy/companion/src/server/provider/instagram/graph/index.js +++ b/packages/@uppy/companion/src/server/provider/instagram/graph/index.js @@ -37,7 +37,7 @@ class Instagram extends Provider { async list ({ directory, token, query = { cursor: null } }) { return this.#withErrorHandling('provider.instagram.list.error', async () => { - const qs = { fields: 'id,media_type,thumbnail_url,media_url,timestamp,children{media_type,media_url,thumbnail_url,timestamp}' } + const qs = { fields: 'id,media_type,thumbnail_url,media_url,timestamp,children{media_type,media_url,thumbnail_url,timestamp}', limit: 5 } if (query.cursor) qs.after = query.cursor diff --git a/packages/@uppy/core/src/Uppy.ts b/packages/@uppy/core/src/Uppy.ts index cea4386535..da5d6fb7f9 100644 --- a/packages/@uppy/core/src/Uppy.ts +++ b/packages/@uppy/core/src/Uppy.ts @@ -61,6 +61,19 @@ export type UnknownPlugin< PluginState extends Record = Record, > = BasePlugin +export type StatusInPartialTree = "checked" | "unchecked" | "partial" + +export type FileInPartialTree = { + id: string + // really .cached is always a `boolean` for "folder"s, and `null` for "file"s + cached: boolean | null + status: StatusInPartialTree + parentId: string | null + data: CompanionFile +} + +export type PartialTree = FileInPartialTree[] + export type UnknownProviderPluginState = { authenticated: boolean | undefined breadcrumbs: { @@ -69,12 +82,11 @@ export type UnknownProviderPluginState = { id?: string }[] didFirstRender: boolean - currentSelection: CompanionFile[] filterInput: string loading: boolean | string - folders: CompanionFile[] - files: CompanionFile[] isSearchVisible: boolean + partialTree: PartialTree + currentFolderId: string | null } /* * UnknownProviderPlugin can be any Companion plugin (such as Google Drive). @@ -116,11 +128,9 @@ export type UnknownSearchProviderPluginState = { } & Pick< UnknownProviderPluginState, | 'loading' - | 'files' - | 'folders' - | 'currentSelection' | 'filterInput' | 'didFirstRender' + | 'partialTree' > export type UnknownSearchProviderPlugin< M extends Meta, diff --git a/packages/@uppy/google-drive/src/DriveProviderViews.ts b/packages/@uppy/google-drive/src/DriveProviderViews.ts index 0aa18d39ea..cddee425d1 100644 --- a/packages/@uppy/google-drive/src/DriveProviderViews.ts +++ b/packages/@uppy/google-drive/src/DriveProviderViews.ts @@ -1,18 +1,18 @@ +import type { FileInPartialTree } from '@uppy/core/lib/Uppy' import { ProviderViews } from '@uppy/provider-views' -import type { CompanionFile } from '@uppy/utils/lib/CompanionFile' import type { Body, Meta } from '@uppy/utils/lib/UppyFile' export default class DriveProviderViews< M extends Meta, B extends Body, > extends ProviderViews { - toggleCheckbox = (e: Event, file: CompanionFile): void => { - e.stopPropagation() - e.preventDefault() + // toggleCheckbox = (e: Event, file: FileInPartialTree): void => { + // e.stopPropagation() + // e.preventDefault() - // Shared Drives aren't selectable; for all else, defer to the base ProviderView. - if (!file.custom!.isSharedDrive) { - super.toggleCheckbox(e, file) - } - } + // // Shared Drives aren't selectable; for all else, defer to the base ProviderView. + // if (!file.data.custom!.isSharedDrive) { + // super.toggleCheckbox(e, file) + // } + // } } diff --git a/packages/@uppy/provider-views/src/Browser.tsx b/packages/@uppy/provider-views/src/Browser.tsx index 731f0eb186..77e7ebe107 100644 --- a/packages/@uppy/provider-views/src/Browser.tsx +++ b/packages/@uppy/provider-views/src/Browser.tsx @@ -14,6 +14,7 @@ import type Uppy from '@uppy/core' import SearchFilterInput from './SearchFilterInput.tsx' import FooterActions from './FooterActions.tsx' import Item from './Item/index.tsx' +import type { FileInPartialTree, PartialTree } from '@uppy/core/lib/Uppy.ts' const VIRTUAL_SHARED_DIR = 'shared-with-me' @@ -21,14 +22,13 @@ type ListItemProps = { currentSelection: any[] uppyFiles: UppyFile[] viewType: string - isChecked: (file: any) => boolean - toggleCheckbox: (event: Event, file: CompanionFile) => void + toggleCheckbox: (event: Event, file: FileInPartialTree) => void recordShiftKeyPress: (event: KeyboardEvent | MouseEvent) => void showTitles: boolean i18n: I18n validateRestrictions: Uppy['validateRestrictions'] - getNextFolder?: (folder: any) => void - f: CompanionFile + getNextFolder?: (folder: FileInPartialTree) => void + f: FileInPartialTree } function ListItem( @@ -38,7 +38,6 @@ function ListItem( currentSelection, uppyFiles, viewType, - isChecked, toggleCheckbox, recordShiftKeyPress, showTitles, @@ -48,15 +47,15 @@ function ListItem( f, } = props - if (f.isFolder) { + if (f.data?.isFolder) { return Item({ showTitles, viewType, i18n, id: f.id, - title: f.name, - getItemIcon: () => f.icon, - status: isChecked(f), + title: f.data!.name, + getItemIcon: () => f.data!.icon, + status: f.status, toggleCheckbox: (event: Event) => toggleCheckbox(event, f), recordShiftKeyPress, type: 'folder', @@ -67,39 +66,37 @@ function ListItem( handleFolderClick: () => getNextFolder!(f), }) } - const restrictionError = validateRestrictions(remoteFileObjToLocal(f), [ + const restrictionError = validateRestrictions(remoteFileObjToLocal(f.data!), [ ...uppyFiles, ...currentSelection, ]) return Item({ id: f.id, - title: f.name, - author: f.author, - getItemIcon: () => f.icon, + title: f.data!.name, + author: f.data!.author, + getItemIcon: () => f.data!.icon, toggleCheckbox: (event: Event) => toggleCheckbox(event, f), isCheckboxDisabled: false, - status: isChecked(f), + status: f.status, recordShiftKeyPress, showTitles, viewType, i18n, type: 'file', - isDisabled: Boolean(restrictionError) && !isChecked(f), + isDisabled: Boolean(restrictionError), restrictionError, }) } type BrowserProps = { - currentSelection: any[] - folders: CompanionFile[] - files: CompanionFile[] + displayedPartialTree: PartialTree, + currentSelection: FileInPartialTree[], uppyFiles: UppyFile[] viewType: string headerComponent?: JSX.Element showBreadcrumbs: boolean - isChecked: (file: any) => boolean - toggleCheckbox: (event: Event, file: CompanionFile) => void + toggleCheckbox: (event: Event, file: FileInPartialTree) => void recordShiftKeyPress: (event: KeyboardEvent | MouseEvent) => void handleScroll: (event: Event) => Promise showTitles: boolean @@ -124,14 +121,11 @@ function Browser( props: BrowserProps, ): JSX.Element { const { - currentSelection, - folders, - files, + displayedPartialTree, uppyFiles, viewType, headerComponent, showBreadcrumbs, - isChecked, toggleCheckbox, recordShiftKeyPress, handleScroll, @@ -151,12 +145,11 @@ function Browser( done, noResultsLabel, loadAllFiles, + currentSelection } = props const selected = currentSelection.length - const rows = useMemo(() => [...folders, ...files], [folders, files]) - return (
    ( ) } - if (!folders.length && !files.length) { + if (displayedPartialTree.length === 0) { return
    {noResultsLabel}
    } @@ -209,13 +202,12 @@ function Browser(
      ( + data={displayedPartialTree} + renderRow={(f: FileInPartialTree) => ( ( // making
        not focusable for firefox tabIndex={-1} > - {rows.map((f) => ( + {displayedPartialTree.map((f) => ( = { className: string isDisabled: boolean restrictionError?: RestrictionError | null - isChecked: boolean + status: StatusInPartialTree | null title?: string itemIconEl: any showTitles?: boolean @@ -25,7 +26,7 @@ function GridListItem( className, isDisabled, restrictionError, - isChecked, + status, title, itemIconEl, showTitles, @@ -35,11 +36,20 @@ function GridListItem( children, } = props + let statusClassName + if (status === "checked") { + statusClassName = "uppy-ProviderBrowserItem-checkbox--is-checked" + } else if (status === "unchecked") { + statusClassName = "" + } else if (status === "partial") { + statusClassName = "uppy-ProviderBrowserItem-checkbox--is-partial" + } + const checkBoxClassName = classNames( 'uppy-u-reset', 'uppy-ProviderBrowserItem-checkbox', 'uppy-ProviderBrowserItem-checkbox--grid', - { 'uppy-ProviderBrowserItem-checkbox--is-checked': isChecked }, + statusClassName ) return ( @@ -56,7 +66,7 @@ function GridListItem( onMouseDown={recordShiftKeyPress} name="listitem" id={id} - checked={isChecked} + checked={status === "checked" ? true : false} disabled={isDisabled} data-uppy-super-focusable /> diff --git a/packages/@uppy/provider-views/src/Item/components/ListLi.tsx b/packages/@uppy/provider-views/src/Item/components/ListLi.tsx index 2b69edaeb2..3ead8ca714 100644 --- a/packages/@uppy/provider-views/src/Item/components/ListLi.tsx +++ b/packages/@uppy/provider-views/src/Item/components/ListLi.tsx @@ -1,5 +1,6 @@ /* eslint-disable react/require-default-props */ import type { RestrictionError } from '@uppy/core/lib/Restricter' +import type { StatusInPartialTree } from '@uppy/core/lib/Uppy' import type { Body, Meta } from '@uppy/utils/lib/UppyFile' import { h } from 'preact' @@ -15,7 +16,7 @@ type ListItemProps = { isDisabled: boolean restrictionError?: RestrictionError | null isCheckboxDisabled: boolean - status: string + status: StatusInPartialTree | null toggleCheckbox: (event: Event) => void recordShiftKeyPress: (event: KeyboardEvent | MouseEvent) => void type: string @@ -73,7 +74,6 @@ export default function ListItem( name="listitem" id={id} checked={status === "checked" ? true : false} - indeterminate={status === "indeterminate" ? true : false} aria-label={type === 'file' ? null : i18n('allFilesFromFolderNamed', { name: title })} disabled={isDisabled} data-uppy-super-focusable diff --git a/packages/@uppy/provider-views/src/Item/index.tsx b/packages/@uppy/provider-views/src/Item/index.tsx index 97040c1373..1a5f64c264 100644 --- a/packages/@uppy/provider-views/src/Item/index.tsx +++ b/packages/@uppy/provider-views/src/Item/index.tsx @@ -9,6 +9,7 @@ import type { Meta, Body } from '@uppy/utils/lib/UppyFile' import ItemIcon from './components/ItemIcon.tsx' import GridListItem from './components/GridLi.tsx' import ListItem from './components/ListLi.tsx' +import type { StatusInPartialTree } from '@uppy/core/lib/Uppy.ts' type ItemProps = { showTitles: boolean @@ -23,7 +24,7 @@ type ItemProps = { type: 'folder' | 'file' author?: CompanionFile['author'] getItemIcon: () => string - isChecked: boolean + status: StatusInPartialTree | null isDisabled: boolean viewType: string } @@ -31,12 +32,12 @@ type ItemProps = { export default function Item( props: ItemProps, ): h.JSX.Element { - const { author, getItemIcon, isChecked, isDisabled, viewType } = props + const { author, getItemIcon, status, isDisabled, viewType } = props const itemIconString = getItemIcon() const className = classNames( 'uppy-ProviderBrowserItem', - { 'uppy-ProviderBrowserItem--selected': isChecked }, + // { 'uppy-ProviderBrowserItem--selected': isChecked }, { 'uppy-ProviderBrowserItem--disabled': isDisabled }, { 'uppy-ProviderBrowserItem--noPreview': itemIconString === 'video' }, ) diff --git a/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx b/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx index ac4b5882a0..5dd39e2aa3 100644 --- a/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx +++ b/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx @@ -7,6 +7,8 @@ import type { UnknownProviderPlugin, UnknownProviderPluginState, Uppy, + PartialTree, + FileInPartialTree } from '@uppy/core/lib/Uppy.ts' import type { Body, Meta } from '@uppy/utils/lib/UppyFile' import type { CompanionFile } from '@uppy/utils/lib/CompanionFile.ts' @@ -112,13 +114,10 @@ export default class ProviderView extends View< // Set default state for the plugin this.plugin.setPluginState({ authenticated: undefined, // we don't know yet - files: [], - folders: [], - partialTree: null, - currentRequestPath: null, + partialTree: [], + currentFolderId: null, filterInput: '', isSearchVisible: false, - currentSelection: [], }) this.registerRequestClient() @@ -183,10 +182,7 @@ export default class ProviderView extends View< } } - async #listFilesAndFolders({ - breadcrumbs, - signal, - }: { + async #listFilesAndFolders({ breadcrumbs, signal }: { breadcrumbs: UnknownProviderPluginState['breadcrumbs'] signal: AbortSignal }) { @@ -221,6 +217,7 @@ export default class ProviderView extends View< */ async getFolder(requestPath?: string, name?: string): Promise { this.setLoading(true) + console.log(`____________________________________________GETTING FOLDER "${requestPath}"`); try { await this.#withAbort(async (signal) => { this.lastCheckbox = undefined @@ -250,41 +247,59 @@ export default class ProviderView extends View< const { partialTree } = this.plugin.getPluginState() - - if (!partialTree) { - const newPartialTree = [ - { requestPath: "root", cached: true }, - ...folders.map((folder) => ({ ...folder, status: "unchecked", parentId: "root", cached: false })), - ...files.map((file) => ({ ...file, status: "unchecked", parentId: "root" })), + console.log({ partialTree }); + if (partialTree.length === 0) { + console.log("creating a new partial tree!"); + const newPartialTree : PartialTree = [ + ...folders.map((folder) => ({ + id: folder.requestPath, parentId: (requestPath || null), data: folder, + status: "unchecked", cached: false, + })) as FileInPartialTree[], + ...files.map((file) => ({ + id: file.requestPath, parentId: (requestPath || null), + status: "unchecked", cached: null, data: file + })) as FileInPartialTree[] ] + console.log({ newPartialTree }); + this.plugin.setPluginState({ partialTree: newPartialTree }) } else { - const clickedFolder = partialTree.find((folder) => folder.requestPath === (requestPath || "root")) + console.log("appending to existing partial tree!"); + const clickedFolder : FileInPartialTree = partialTree.find((folder) => folder.id === requestPath)! // If selected folder is already filled in, don't refill it (because that would make it lose deep state!) // Otherwise, cache the current folder! - if (!clickedFolder.cached) { - const clickedFolderContents = [ - ...folders.map((folder) => ({ ...folder, status: clickedFolder.status, parentId: clickedFolder.requestPath, cached: false })), - ...files.map((file) => ({ ...file, status: clickedFolder.status, parentId: clickedFolder.requestPath })), + if (clickedFolder && !clickedFolder.cached) { + const clickedFolderContents : FileInPartialTree[] = [ + ...folders.map((folder) => ({ + id: folder.requestPath, parentId: clickedFolder.id, data: folder, + status: clickedFolder.status, cached: false, + })), + ...files.map((file) => ({ + id: file.requestPath, parentId: clickedFolder.id, data: file, + status: clickedFolder.status, cached: null, + })), ] // just doing `clickedFolder.cached = true` in a non-mutating way - const updatedClickedFolder = { ...clickedFolder, cached: true } + const updatedClickedFolder : FileInPartialTree = { ...clickedFolder, cached: true } const partialTreeWithUpdatedClickedFolder = partialTree.map((folder) => - folder.requestPath === updatedClickedFolder.requestPath ? + folder.id === updatedClickedFolder.id ? updatedClickedFolder : folder ) - this.plugin.setPluginState({ partialTree: [ - ...partialTreeWithUpdatedClickedFolder, - ...clickedFolderContents] }) + this.plugin.setPluginState({ + partialTree: [ + ...partialTreeWithUpdatedClickedFolder, + ...clickedFolderContents + ] + }) } } - this.plugin.setPluginState({ folders, files, currentRequestPath: requestPath, filterInput: '' }) + this.plugin.setPluginState({ currentFolderId: (requestPath || null), filterInput: '' }) }) @@ -318,8 +333,8 @@ export default class ProviderView extends View< /** * Fetches new folder */ - getNextFolder(folder: CompanionFile): void { - this.getFolder(folder.requestPath, folder.name) + getNextFolder(folder: FileInPartialTree): void { + this.getFolder(folder.data.requestPath, folder.data.name) this.lastCheckbox = undefined } @@ -348,10 +363,8 @@ export default class ProviderView extends View< const newState = { authenticated: false, - currentRequestPath: null, - partialTree: null, - files: [], - folders: [], + currentFolderId: null, + partialTree: [], filterInput: '', } this.plugin.setPluginState(newState) @@ -395,24 +408,31 @@ export default class ProviderView extends View< } async handleScroll(event: Event): Promise { + console.log("handleScrolll"); if (this.shouldHandleScroll(event) && this.nextPagePath) { this.isHandlingScroll = true try { await this.#withAbort(async (signal) => { - const { files, folders } = this.plugin.getPluginState() + const { partialTree } = this.plugin.getPluginState() - const { files: newFiles, folders: newFolders } = await this.#listFilesAndFolders({ + const { files, folders } = await this.#listFilesAndFolders({ breadcrumbs: this.getBreadcrumbs(), signal, }) - const combinedFiles = files.concat(newFiles) - const combinedFolders = folders.concat(newFolders) + const newPartialTree = [ + ...partialTree, + ...folders.map((folder) => ({ + id: folder.requestPath, parentId: this.nextPagePath, data: folder, + status: "unchecked", cached: false, + })) as FileInPartialTree[], + ...files.map((file) => ({ + id: file.requestPath, parentId: this.nextPagePath, + status: "unchecked", cached: null, data: file + })) as FileInPartialTree[] + ] - this.plugin.setPluginState({ - folders: combinedFolders, - files: combinedFiles, - }) + this.plugin.setPluginState({ partialTree: newPartialTree }) }) } catch (error) { this.handleError(error) @@ -469,24 +489,25 @@ export default class ProviderView extends View< this.setLoading(true) try { await this.#withAbort(async (signal) => { - const { currentSelection } = this.plugin.getPluginState() + const { partialTree } = this.plugin.getPluginState() + const currentSelection = partialTree.filter((item) => item.status === "checked") const messages: string[] = [] const newFiles: CompanionFile[] = [] for (const selectedItem of currentSelection) { - const { requestPath } = selectedItem + const requestPath = selectedItem.id const withRelDirPath = (newItem: CompanionFile) => ({ ...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 as string) - .replace(selectedItem.absDirPath as string, '') + .replace(selectedItem.data!.absDirPath as string, '') .replace(/^\//, ''), }) - if (selectedItem.isFolder) { + if (selectedItem.data!.isFolder) { let isEmpty = true let numNewFiles = 0 @@ -517,10 +538,10 @@ export default class ProviderView extends View< await this.#recursivelyListAllFiles({ requestPath, absDirPath: prependPath( - selectedItem.absDirPath, - selectedItem.name, + selectedItem.data!.absDirPath, + selectedItem.data!.name, ), - relDirPath: selectedItem.name, + relDirPath: selectedItem.data!.name, queue, onFiles, signal, @@ -532,7 +553,7 @@ export default class ProviderView extends View< message = this.plugin.uppy.i18n('emptyFolderAdded') } else if (numNewFiles === 0) { message = this.plugin.uppy.i18n('folderAlreadyAdded', { - folder: selectedItem.name, + folder: selectedItem.data!.name, }) } else { // TODO we don't really know at this point whether any files were actually added @@ -540,13 +561,13 @@ export default class ProviderView extends View< // 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, + folder: selectedItem.data!.name, }) } messages.push(message) } else { - newFiles.push(withRelDirPath(selectedItem)) + newFiles.push(withRelDirPath(selectedItem.data!)) } } @@ -576,14 +597,14 @@ export default class ProviderView extends View< } getBreadcrumbs = () => { - const { partialTree, currentRequestPath } = this.plugin.getPluginState() + const { partialTree, currentFolderId } = this.plugin.getPluginState() const breadcrumbs = [] - if (partialTree && currentRequestPath) { - const currentFolder = partialTree.find((folder) => folder.requestPath === currentRequestPath) + if (partialTree && currentFolderId) { + const currentFolder = partialTree.find((folder) => folder.id === currentFolderId) let parent = currentFolder while (parent) { breadcrumbs.push(parent) - parent = partialTree.find((folder) => folder.requestPath === parent.parentId) + parent = partialTree.find((folder) => folder.id === parent!.parentId) } } return breadcrumbs.toReversed() @@ -601,10 +622,9 @@ export default class ProviderView extends View< } const targetViewOptions = { ...this.opts, ...viewOptions } - const { files, folders, filterInput, loading, currentSelection } = + const { partialTree, currentFolderId, filterInput, loading } = this.plugin.getPluginState() - const { isChecked, toggleCheckbox, recordShiftKeyPress, filterItems } = this - const hasInput = filterInput !== '' + const { toggleCheckbox, recordShiftKeyPress, filterItems } = this const pluginIcon = this.plugin.icon || defaultPickerIcon const headerProps = { @@ -618,13 +638,17 @@ export default class ProviderView extends View< i18n, } + // console.log("_______________________rendering_________________"); + + const displayedPartialTree = filterItems(partialTree.filter((item) => item.parentId === currentFolderId)) + // console.log({ partialTree, displayedPartialTree, currentFolderId }); + + // console.log("________________________________________________"); + const browserProps = { - isChecked, toggleCheckbox, recordShiftKeyPress, - currentSelection, - files: hasInput ? filterItems(files) : files, - folders: hasInput ? filterItems(folders) : folders, + displayedPartialTree, getNextFolder: this.getNextFolder, getFolder: this.getFolder, loadAllFiles: this.opts.loadAllFiles, @@ -637,6 +661,7 @@ export default class ProviderView extends View< searchOnInput: true, searchInputLabel: i18n('filter'), clearSearchLabel: i18n('resetFilter'), + currentSelection: partialTree.filter((item) => item.status === "checked"), noResultsLabel: i18n('noFilesFound'), logout: this.logout, diff --git a/packages/@uppy/provider-views/src/View.ts b/packages/@uppy/provider-views/src/View.ts index 0e8aae691a..dcded79b3d 100644 --- a/packages/@uppy/provider-views/src/View.ts +++ b/packages/@uppy/provider-views/src/View.ts @@ -1,4 +1,7 @@ import type { + FileInPartialTree, + PartialTree, + StatusInPartialTree, UnknownProviderPlugin, UnknownSearchProviderPlugin, } from '@uppy/core/lib/Uppy' @@ -83,7 +86,7 @@ export default class View< } clearSelection(): void { - this.plugin.setPluginState({ currentSelection: [], filterInput: '' }) + this.plugin.setPluginState({ filterInput: '' }) } cancelPicking(): void { @@ -175,14 +178,14 @@ export default class View< return tagFile } - filterItems = (items: CompanionFile[]): CompanionFile[] => { + filterItems = (items: FileInPartialTree[]): FileInPartialTree[] => { const state = this.plugin.getPluginState() if (!state.filterInput || state.filterInput === '') { return items } - return items.filter((folder) => { + return items.filter((item) => { return ( - folder.name.toLowerCase().indexOf(state.filterInput.toLowerCase()) !== + item.data?.name.toLowerCase().indexOf(state.filterInput.toLowerCase()) !== -1 ) }) @@ -199,22 +202,22 @@ export default class View< * toggle multiple checkboxes at once, which is done by getting all files * in between last checked file and current one. */ - toggleCheckbox = (e, fileOrFolder) => { + toggleCheckbox = (e: Event, ourItem: FileInPartialTree) => { + console.log({ ourItem }); e.stopPropagation() e.preventDefault() - e.currentTarget.focus() const { partialTree } = this.plugin.getPluginState() - const newPartialTree = JSON.parse(JSON.stringify(partialTree)) + const newPartialTree : PartialTree = JSON.parse(JSON.stringify(partialTree)) - const ourItem = newPartialTree.find((item) => item.requestPath === fileOrFolder.requestPath) const newStatus = ourItem.status === "checked" ? "unchecked" : "checked" - ourItem.status = newStatus + const ourItemInNewTree = newPartialTree.find((item) => item.id === ourItem.id)! + ourItemInNewTree.status = newStatus // if newStatus is "checked" - percolate down "checked" // if newStatus is "unchecked" - percolate down "unchecked" - const percolateDown = (currentFile, status) => { - const children = newPartialTree.filter((item) => item.parentId === currentFile.requestPath) + const percolateDown = (currentFile: FileInPartialTree, status: StatusInPartialTree) => { + const children : FileInPartialTree[] = newPartialTree.filter((item) => item.parentId === currentFile.id) as FileInPartialTree[] children.forEach((item) => { item.status = status percolateDown(item, status) @@ -224,12 +227,12 @@ export default class View< percolateDown(ourItem, newStatus) // we do something to all of its parents. - const percolateUp = (currentFile) => { - const parentFolder = newPartialTree.find((item) => item.requestPath === currentFile.parentId) + const percolateUp = (currentFile: FileInPartialTree) => { + const parentFolder = newPartialTree.find((item) => item.id === currentFile.parentId) if (!parentFolder) return - const parentsChildren = newPartialTree.filter((item) => item.parentId === parentFolder.requestPath) + const parentsChildren = newPartialTree.filter((item) => item.parentId === parentFolder.id) as FileInPartialTree[] const areAllChildrenChecked = parentsChildren.every((item) => item.status === "checked") const areAllChildrenUnchecked = parentsChildren.every((item) => item.status === "unchecked") @@ -249,21 +252,6 @@ export default class View< this.plugin.setPluginState({ partialTree: newPartialTree }) } - isChecked = (file) => { - // Ohhh so it's actually called here...... - - const { partialTree } = this.plugin.getPluginState() - const ourFile = partialTree.find((item) => item.requestPath === file.requestPath) - // console.log({ourFile}); - - return ourFile.status - - // 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: boolean | string): void { this.plugin.setPluginState({ loading }) } From ab42ad64027257c7fae0c0e6783a004bcb55c190 Mon Sep 17 00:00:00 2001 From: Evgenia Karunus Date: Thu, 28 Mar 2024 02:21:20 +0400 Subject: [PATCH 005/170] GoogleDrive - made breadcrumbs work --- packages/@uppy/core/src/Uppy.ts | 5 ----- packages/@uppy/provider-views/src/Breadcrumbs.tsx | 14 ++++++++++---- .../src/ProviderView/ProviderView.tsx | 11 +++++------ 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/packages/@uppy/core/src/Uppy.ts b/packages/@uppy/core/src/Uppy.ts index da5d6fb7f9..d5815e8815 100644 --- a/packages/@uppy/core/src/Uppy.ts +++ b/packages/@uppy/core/src/Uppy.ts @@ -76,11 +76,6 @@ export type PartialTree = FileInPartialTree[] export type UnknownProviderPluginState = { authenticated: boolean | undefined - breadcrumbs: { - requestPath?: string - name?: string - id?: string - }[] didFirstRender: boolean filterInput: string loading: boolean | string diff --git a/packages/@uppy/provider-views/src/Breadcrumbs.tsx b/packages/@uppy/provider-views/src/Breadcrumbs.tsx index 3a053d5a8f..75adca5202 100644 --- a/packages/@uppy/provider-views/src/Breadcrumbs.tsx +++ b/packages/@uppy/provider-views/src/Breadcrumbs.tsx @@ -1,4 +1,4 @@ -import type { UnknownProviderPluginState } from '@uppy/core/lib/Uppy' +import type { FileInPartialTree, UnknownProviderPluginState } from '@uppy/core/lib/Uppy' import { h, Fragment } from 'preact' import type { Body, Meta } from '@uppy/utils/lib/UppyFile' import type ProviderView from './ProviderView' @@ -30,7 +30,7 @@ type BreadcrumbsProps = { getFolder: ProviderView['getFolder'] title: string breadcrumbsIcon: JSX.Element - breadcrumbs: UnknownProviderPluginState['breadcrumbs'] + breadcrumbs: FileInPartialTree[] } export default function Breadcrumbs( @@ -41,11 +41,17 @@ export default function Breadcrumbs( return (
        {breadcrumbsIcon}
        + getFolder("root")} + title={title} + isLast={breadcrumbs.length === 0} + /> {breadcrumbs.map((directory, i) => ( getFolder(directory.requestPath, directory.name)} - title={i === 0 ? title : (directory.name as string)} + getFolder={() => getFolder(directory.data.requestPath, directory.data.name)} + title={directory.data.name} isLast={i + 1 === breadcrumbs.length} /> ))} diff --git a/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx b/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx index 5dd39e2aa3..4131517387 100644 --- a/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx +++ b/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx @@ -24,12 +24,9 @@ import View, { type ViewOptions } from '../View.ts' // @ts-ignore We don't want TS to generate types for the package.json import packageJson from '../../package.json' -function formatBreadcrumbs( - breadcrumbs: UnknownProviderPluginState['breadcrumbs'], -): string { +function formatBreadcrumbs(breadcrumbs: FileInPartialTree[]): string { return breadcrumbs - .slice(1) - .map((directory) => directory.name) + .map((directory) => directory.data.name) .join('/') } @@ -183,7 +180,7 @@ export default class ProviderView extends View< } async #listFilesAndFolders({ breadcrumbs, signal }: { - breadcrumbs: UnknownProviderPluginState['breadcrumbs'] + breadcrumbs: FileInPartialTree[], signal: AbortSignal }) { const absDirPath = formatBreadcrumbs(breadcrumbs) @@ -607,6 +604,8 @@ export default class ProviderView extends View< parent = partialTree.find((folder) => folder.id === parent!.parentId) } } + console.log("_____________________________calculated breadcrumbs:"); + console.log({ breadcrumbs }); return breadcrumbs.toReversed() } From 1ba90c7c7571d1ca5d854513863fbc3afe419f9f Mon Sep 17 00:00:00 2001 From: Evgenia Karunus Date: Thu, 28 Mar 2024 02:33:19 +0400 Subject: [PATCH 006/170] .getFolder() - remove the `name` argument --- packages/@uppy/provider-views/src/Breadcrumbs.tsx | 2 +- .../@uppy/provider-views/src/ProviderView/ProviderView.tsx | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/@uppy/provider-views/src/Breadcrumbs.tsx b/packages/@uppy/provider-views/src/Breadcrumbs.tsx index 75adca5202..50eb59a970 100644 --- a/packages/@uppy/provider-views/src/Breadcrumbs.tsx +++ b/packages/@uppy/provider-views/src/Breadcrumbs.tsx @@ -50,7 +50,7 @@ export default function Breadcrumbs( {breadcrumbs.map((directory, i) => ( getFolder(directory.data.requestPath, directory.data.name)} + getFolder={() => getFolder(directory.data.requestPath)} title={directory.data.name} isLast={i + 1 === breadcrumbs.length} /> diff --git a/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx b/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx index 4131517387..a27e60f60e 100644 --- a/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx +++ b/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx @@ -212,7 +212,7 @@ export default class ProviderView extends View< * TODO rename to something better like selectFolder or navigateToFolder (breaking change?) * */ - async getFolder(requestPath?: string, name?: string): Promise { + async getFolder(requestPath?: string): Promise { this.setLoading(true) console.log(`____________________________________________GETTING FOLDER "${requestPath}"`); try { @@ -331,7 +331,7 @@ export default class ProviderView extends View< * Fetches new folder */ getNextFolder(folder: FileInPartialTree): void { - this.getFolder(folder.data.requestPath, folder.data.name) + this.getFolder(folder.data.requestPath) this.lastCheckbox = undefined } From ec532a2511670e629acd8a5756ee1690f3560403 Mon Sep 17 00:00:00 2001 From: Evgenia Karunus Date: Thu, 28 Mar 2024 02:43:57 +0400 Subject: [PATCH 007/170] - refactors "/" --- packages/@uppy/provider-views/src/Breadcrumbs.tsx | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/packages/@uppy/provider-views/src/Breadcrumbs.tsx b/packages/@uppy/provider-views/src/Breadcrumbs.tsx index 50eb59a970..967131f675 100644 --- a/packages/@uppy/provider-views/src/Breadcrumbs.tsx +++ b/packages/@uppy/provider-views/src/Breadcrumbs.tsx @@ -6,14 +6,15 @@ import type ProviderView from './ProviderView' type BreadcrumbProps = { getFolder: () => void title: string - isLast: boolean + isFirst?: boolean } const Breadcrumb = (props: BreadcrumbProps) => { - const { getFolder, title, isLast } = props + const { getFolder, title, isFirst } = props return ( + {!isFirst ? ' / ' : ''} - {!isLast ? ' / ' : ''} ) } @@ -45,14 +45,13 @@ export default function Breadcrumbs( key="root" getFolder={() => getFolder("root")} title={title} - isLast={breadcrumbs.length === 0} + isFirst /> {breadcrumbs.map((directory, i) => ( getFolder(directory.data.requestPath)} title={directory.data.name} - isLast={i + 1 === breadcrumbs.length} /> ))}
        From 51fc807710c9ebdef522b58d60f8148ff3708777 Mon Sep 17 00:00:00 2001 From: Evgenia Karunus Date: Thu, 28 Mar 2024 03:03:46 +0400 Subject: [PATCH 008/170] Instagram - made files get fetched onScroll --- .../provider-views/src/ProviderView/ProviderView.tsx | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx b/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx index a27e60f60e..82fa843ef7 100644 --- a/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx +++ b/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx @@ -411,7 +411,7 @@ export default class ProviderView extends View< try { await this.#withAbort(async (signal) => { - const { partialTree } = this.plugin.getPluginState() + const { partialTree, currentFolderId } = this.plugin.getPluginState() const { files, folders } = await this.#listFilesAndFolders({ breadcrumbs: this.getBreadcrumbs(), signal, @@ -420,12 +420,12 @@ export default class ProviderView extends View< const newPartialTree = [ ...partialTree, ...folders.map((folder) => ({ - id: folder.requestPath, parentId: this.nextPagePath, data: folder, + id: folder.requestPath, parentId: currentFolderId, data: folder, status: "unchecked", cached: false, })) as FileInPartialTree[], ...files.map((file) => ({ - id: file.requestPath, parentId: this.nextPagePath, - status: "unchecked", cached: null, data: file + id: file.requestPath, parentId: currentFolderId, data: file, + status: "unchecked", cached: null })) as FileInPartialTree[] ] @@ -604,8 +604,6 @@ export default class ProviderView extends View< parent = partialTree.find((folder) => folder.id === parent!.parentId) } } - console.log("_____________________________calculated breadcrumbs:"); - console.log({ breadcrumbs }); return breadcrumbs.toReversed() } From ba0b0977ceea7bf04aaedaf2881ed203f102a051 Mon Sep 17 00:00:00 2001 From: Evgenia Karunus Date: Fri, 29 Mar 2024 01:36:35 +0400 Subject: [PATCH 009/170] clearSelection() - recover the functionality --- packages/@uppy/provider-views/src/Browser.tsx | 4 ++-- packages/@uppy/provider-views/src/View.ts | 8 ++++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/@uppy/provider-views/src/Browser.tsx b/packages/@uppy/provider-views/src/Browser.tsx index 77e7ebe107..721de704a7 100644 --- a/packages/@uppy/provider-views/src/Browser.tsx +++ b/packages/@uppy/provider-views/src/Browser.tsx @@ -47,7 +47,7 @@ function ListItem( f, } = props - if (f.data?.isFolder) { + if (f.data.isFolder) { return Item({ showTitles, viewType, @@ -66,7 +66,7 @@ function ListItem( handleFolderClick: () => getNextFolder!(f), }) } - const restrictionError = validateRestrictions(remoteFileObjToLocal(f.data!), [ + const restrictionError = validateRestrictions(remoteFileObjToLocal(f.data), [ ...uppyFiles, ...currentSelection, ]) diff --git a/packages/@uppy/provider-views/src/View.ts b/packages/@uppy/provider-views/src/View.ts index dcded79b3d..341edf583e 100644 --- a/packages/@uppy/provider-views/src/View.ts +++ b/packages/@uppy/provider-views/src/View.ts @@ -86,7 +86,12 @@ export default class View< } clearSelection(): void { - this.plugin.setPluginState({ filterInput: '' }) + const { partialTree } = this.plugin.getPluginState() + const newPartialTree : PartialTree = partialTree.map((item) => ({ + ...item, + status: "unchecked" + })) + this.plugin.setPluginState({ partialTree: newPartialTree, filterInput: '' }) } cancelPicking(): void { @@ -203,7 +208,6 @@ export default class View< * in between last checked file and current one. */ toggleCheckbox = (e: Event, ourItem: FileInPartialTree) => { - console.log({ ourItem }); e.stopPropagation() e.preventDefault() From dc589c8805d1c102516d39d0690b11c1f965f39b Mon Sep 17 00:00:00 2001 From: Evgenia Karunus Date: Fri, 29 Mar 2024 02:01:20 +0400 Subject: [PATCH 010/170] GoogleDrive - recover custom `.toggleCheckbox()` functionality --- .../@uppy/google-drive/src/DriveProviderViews.ts | 16 ++++++++-------- packages/@uppy/provider-views/src/View.ts | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/@uppy/google-drive/src/DriveProviderViews.ts b/packages/@uppy/google-drive/src/DriveProviderViews.ts index cddee425d1..9ca535bfa6 100644 --- a/packages/@uppy/google-drive/src/DriveProviderViews.ts +++ b/packages/@uppy/google-drive/src/DriveProviderViews.ts @@ -6,13 +6,13 @@ export default class DriveProviderViews< M extends Meta, B extends Body, > extends ProviderViews { - // toggleCheckbox = (e: Event, file: FileInPartialTree): void => { - // e.stopPropagation() - // e.preventDefault() + toggleCheckbox = (e: Event, file: FileInPartialTree): void => { + e.stopPropagation() + e.preventDefault() - // // Shared Drives aren't selectable; for all else, defer to the base ProviderView. - // if (!file.data.custom!.isSharedDrive) { - // super.toggleCheckbox(e, file) - // } - // } + // Shared Drives aren't selectable; for all else, defer to the base ProviderView. + if (!file.data.custom!.isSharedDrive) { + super.toggleCheckbox(e, file) + } + } } diff --git a/packages/@uppy/provider-views/src/View.ts b/packages/@uppy/provider-views/src/View.ts index 341edf583e..47a832eb8b 100644 --- a/packages/@uppy/provider-views/src/View.ts +++ b/packages/@uppy/provider-views/src/View.ts @@ -207,7 +207,7 @@ export default class View< * toggle multiple checkboxes at once, which is done by getting all files * in between last checked file and current one. */ - toggleCheckbox = (e: Event, ourItem: FileInPartialTree) => { + toggleCheckbox(e: Event, ourItem: FileInPartialTree) { e.stopPropagation() e.preventDefault() From 55ef7841ea586c1941fd729c09ebd711e34c3150 Mon Sep 17 00:00:00 2001 From: Evgenia Karunus Date: Fri, 29 Mar 2024 02:19:52 +0400 Subject: [PATCH 011/170] providers - recover `.isDisabled` functionality --- packages/@uppy/provider-views/src/Browser.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@uppy/provider-views/src/Browser.tsx b/packages/@uppy/provider-views/src/Browser.tsx index 721de704a7..364e13716f 100644 --- a/packages/@uppy/provider-views/src/Browser.tsx +++ b/packages/@uppy/provider-views/src/Browser.tsx @@ -84,7 +84,7 @@ function ListItem( viewType, i18n, type: 'file', - isDisabled: Boolean(restrictionError), + isDisabled: Boolean(restrictionError) && (f.status !== "checked"), restrictionError, }) } From 5f356a0f6fa48d0cff78899702439c802c8197c7 Mon Sep 17 00:00:00 2001 From: Evgenia Karunus Date: Fri, 29 Mar 2024 03:47:40 +0400 Subject: [PATCH 012/170] - made Unsplash use .partialTree --- packages/@uppy/core/src/Uppy.ts | 1 + packages/@uppy/provider-views/src/Browser.tsx | 2 - .../src/ProviderView/ProviderView.tsx | 10 +--- .../SearchProviderView/SearchProviderView.tsx | 53 ++++++++++--------- 4 files changed, 32 insertions(+), 34 deletions(-) diff --git a/packages/@uppy/core/src/Uppy.ts b/packages/@uppy/core/src/Uppy.ts index d5815e8815..f6b120f0ce 100644 --- a/packages/@uppy/core/src/Uppy.ts +++ b/packages/@uppy/core/src/Uppy.ts @@ -126,6 +126,7 @@ export type UnknownSearchProviderPluginState = { | 'filterInput' | 'didFirstRender' | 'partialTree' + | 'currentFolderId' > export type UnknownSearchProviderPlugin< M extends Meta, diff --git a/packages/@uppy/provider-views/src/Browser.tsx b/packages/@uppy/provider-views/src/Browser.tsx index 364e13716f..b24c4f4156 100644 --- a/packages/@uppy/provider-views/src/Browser.tsx +++ b/packages/@uppy/provider-views/src/Browser.tsx @@ -3,11 +3,9 @@ import { h } from 'preact' import classNames from 'classnames' import remoteFileObjToLocal from '@uppy/utils/lib/remoteFileObjToLocal' -import { useMemo } from 'preact/hooks' // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore untyped import VirtualList from '@uppy/utils/lib/VirtualList' -import type { CompanionFile } from '@uppy/utils/lib/CompanionFile' import type { Body, Meta, UppyFile } from '@uppy/utils/lib/UppyFile' import type { I18n } from '@uppy/utils/lib/Translator' import type Uppy from '@uppy/core' diff --git a/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx b/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx index 82fa843ef7..e6293ea904 100644 --- a/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx +++ b/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx @@ -5,7 +5,6 @@ import { getSafeFileId } from '@uppy/utils/lib/generateFileID' import type { UnknownProviderPlugin, - UnknownProviderPluginState, Uppy, PartialTree, FileInPartialTree @@ -621,7 +620,7 @@ export default class ProviderView extends View< const targetViewOptions = { ...this.opts, ...viewOptions } const { partialTree, currentFolderId, filterInput, loading } = this.plugin.getPluginState() - const { toggleCheckbox, recordShiftKeyPress, filterItems } = this + const { recordShiftKeyPress, filterItems } = this const pluginIcon = this.plugin.icon || defaultPickerIcon const headerProps = { @@ -635,15 +634,10 @@ export default class ProviderView extends View< i18n, } - // console.log("_______________________rendering_________________"); - const displayedPartialTree = filterItems(partialTree.filter((item) => item.parentId === currentFolderId)) - // console.log({ partialTree, displayedPartialTree, currentFolderId }); - - // console.log("________________________________________________"); const browserProps = { - toggleCheckbox, + toggleCheckbox: this.toggleCheckbox.bind(this), recordShiftKeyPress, displayedPartialTree, getNextFolder: this.getNextFolder, diff --git a/packages/@uppy/provider-views/src/SearchProviderView/SearchProviderView.tsx b/packages/@uppy/provider-views/src/SearchProviderView/SearchProviderView.tsx index c327546750..ffe65460c9 100644 --- a/packages/@uppy/provider-views/src/SearchProviderView/SearchProviderView.tsx +++ b/packages/@uppy/provider-views/src/SearchProviderView/SearchProviderView.tsx @@ -1,7 +1,7 @@ import { h } from 'preact' import type { Body, Meta } from '@uppy/utils/lib/UppyFile' -import type { UnknownSearchProviderPlugin } from '@uppy/core/lib/Uppy.ts' +import type { PartialTree, StatusInPartialTree, UnknownSearchProviderPlugin } from '@uppy/core/lib/Uppy.ts' import type { DefinePluginOpts } from '@uppy/core/lib/BasePlugin.ts' import type Uppy from '@uppy/core' import type { CompanionFile } from '@uppy/utils/lib/CompanionFile' @@ -16,12 +16,11 @@ import packageJson from '../../package.json' const defaultState = { isInputMode: true, - files: [], - folders: [], breadcrumbs: [], filterInput: '', - currentSelection: [], searchTerm: null, + partialTree: [], + currentFolderId: null, } type PluginType = 'SearchProvider' @@ -85,15 +84,22 @@ export default class SearchProviderView< this.plugin.setPluginState(defaultState) } - #updateFilesAndInputMode(res: Res, files: CompanionFile[]): void { + #updateFilesAndInputMode(res: Res, files: PartialTree): void { this.nextPageQuery = res.nextPageQuery - res.items.forEach((item) => { - files.push(item) - }) + const { partialTree } = this.plugin.getPluginState() + const newPartialTree : PartialTree = [ + ...partialTree, + ...res.items.map((item) => ({ + id: item.requestPath, + cached: null, + status: "unchecked" as StatusInPartialTree, + parentId: null, + data: item + })) + ] this.plugin.setPluginState({ - currentSelection: [], + partialTree: newPartialTree, isInputMode: false, - files, searchTerm: res.searchedFor, }) } @@ -118,8 +124,8 @@ export default class SearchProviderView< clearSearch(): void { this.plugin.setPluginState({ - currentSelection: [], - files: [], + partialTree: [], + currentFolderId: null, searchTerm: null, }) } @@ -131,10 +137,10 @@ export default class SearchProviderView< this.isHandlingScroll = true try { - const { files, searchTerm } = this.plugin.getPluginState() + const { partialTree, searchTerm } = this.plugin.getPluginState() const response = await this.provider.search(searchTerm!, query) - this.#updateFilesAndInputMode(response, files) + this.#updateFilesAndInputMode(response, partialTree) } catch (error) { this.handleError(error) } finally { @@ -144,10 +150,10 @@ export default class SearchProviderView< } donePicking(): void { - const { currentSelection } = this.plugin.getPluginState() + const { partialTree } = this.plugin.getPluginState() this.plugin.uppy.log('Adding remote search provider files') this.plugin.uppy.addFiles( - currentSelection.map((file) => this.getTagFile(file)), + partialTree.map((file) => this.getTagFile(file.data)), ) this.resetPluginState() } @@ -165,18 +171,17 @@ export default class SearchProviderView< } const targetViewOptions = { ...this.opts, ...viewOptions } - const { files, folders, filterInput, loading, currentSelection } = + const { loading, partialTree, currentFolderId } = this.plugin.getPluginState() - const { isChecked, toggleCheckbox, filterItems, recordShiftKeyPress } = this - const hasInput = filterInput !== '' + const { filterItems, recordShiftKeyPress } = this + + const displayedPartialTree = filterItems(partialTree.filter((item) => item.parentId === currentFolderId)) const browserProps = { - isChecked, - toggleCheckbox, + toggleCheckbox: this.toggleCheckbox.bind(this), recordShiftKeyPress, - currentSelection, - files: hasInput ? filterItems(files) : files, - folders: hasInput ? filterItems(folders) : folders, + currentSelection: partialTree.filter((item) => item.status === "checked"), + displayedPartialTree, handleScroll: this.handleScroll, done: this.donePicking, cancel: this.cancelPicking, From 74b01c5597cab922b15c98a9afa6199b2c593e62 Mon Sep 17 00:00:00 2001 From: Evgenia Karunus Date: Fri, 29 Mar 2024 04:08:44 +0400 Subject: [PATCH 013/170] Facebook - change `.files, .folders` => `.partialTree` --- packages/@uppy/facebook/src/Facebook.tsx | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/@uppy/facebook/src/Facebook.tsx b/packages/@uppy/facebook/src/Facebook.tsx index 91c186d1c2..fa870254cf 100644 --- a/packages/@uppy/facebook/src/Facebook.tsx +++ b/packages/@uppy/facebook/src/Facebook.tsx @@ -114,10 +114,9 @@ export default class Facebook extends UIPlugin< showFilter?: boolean showTitles?: boolean } = {} - if ( - this.getPluginState().files.length && - !this.getPluginState().folders.length - ) { + const { partialTree } = this.getPluginState() + const nOfFolders = partialTree.filter((item) => item.data.type === "folder").length + if (nOfFolders === 0) { viewOptions.viewType = 'grid' viewOptions.showFilter = false viewOptions.showTitles = false From 101032c9078876d5f7b92fdd20b0d1086e08bdd1 Mon Sep 17 00:00:00 2001 From: Evgenia Karunus Date: Fri, 29 Mar 2024 04:21:13 +0400 Subject: [PATCH 014/170] everywhere - we don't need to ! `partialTreeFile.data` anymore --- packages/@uppy/provider-views/src/Browser.tsx | 10 +++++----- .../src/ProviderView/ProviderView.tsx | 16 ++++++++-------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/packages/@uppy/provider-views/src/Browser.tsx b/packages/@uppy/provider-views/src/Browser.tsx index b24c4f4156..fbc2d19b8c 100644 --- a/packages/@uppy/provider-views/src/Browser.tsx +++ b/packages/@uppy/provider-views/src/Browser.tsx @@ -51,8 +51,8 @@ function ListItem( viewType, i18n, id: f.id, - title: f.data!.name, - getItemIcon: () => f.data!.icon, + title: f.data.name, + getItemIcon: () => f.data.icon, status: f.status, toggleCheckbox: (event: Event) => toggleCheckbox(event, f), recordShiftKeyPress, @@ -71,9 +71,9 @@ function ListItem( return Item({ id: f.id, - title: f.data!.name, - author: f.data!.author, - getItemIcon: () => f.data!.icon, + title: f.data.name, + author: f.data.author, + getItemIcon: () => f.data.icon, toggleCheckbox: (event: Event) => toggleCheckbox(event, f), isCheckboxDisabled: false, status: f.status, diff --git a/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx b/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx index e6293ea904..ea27f598c9 100644 --- a/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx +++ b/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx @@ -499,11 +499,11 @@ export default class ProviderView extends View< // 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 as string) - .replace(selectedItem.data!.absDirPath as string, '') + .replace(selectedItem.data.absDirPath as string, '') .replace(/^\//, ''), }) - if (selectedItem.data!.isFolder) { + if (selectedItem.data.isFolder) { let isEmpty = true let numNewFiles = 0 @@ -534,10 +534,10 @@ export default class ProviderView extends View< await this.#recursivelyListAllFiles({ requestPath, absDirPath: prependPath( - selectedItem.data!.absDirPath, - selectedItem.data!.name, + selectedItem.data.absDirPath, + selectedItem.data.name, ), - relDirPath: selectedItem.data!.name, + relDirPath: selectedItem.data.name, queue, onFiles, signal, @@ -549,7 +549,7 @@ export default class ProviderView extends View< message = this.plugin.uppy.i18n('emptyFolderAdded') } else if (numNewFiles === 0) { message = this.plugin.uppy.i18n('folderAlreadyAdded', { - folder: selectedItem.data!.name, + folder: selectedItem.data.name, }) } else { // TODO we don't really know at this point whether any files were actually added @@ -557,13 +557,13 @@ export default class ProviderView extends View< // 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.data!.name, + folder: selectedItem.data.name, }) } messages.push(message) } else { - newFiles.push(withRelDirPath(selectedItem.data!)) + newFiles.push(withRelDirPath(selectedItem.data)) } } From e12b26f95de1318031809ac06805a51292d9f74c Mon Sep 17 00:00:00 2001 From: Evgenia Karunus Date: Mon, 1 Apr 2024 01:12:33 +0400 Subject: [PATCH 015/170] - implement folder caching --- .../src/ProviderView/ProviderView.tsx | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx b/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx index ea27f598c9..f905b4e4c0 100644 --- a/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx +++ b/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx @@ -212,9 +212,18 @@ export default class ProviderView extends View< * */ async getFolder(requestPath?: string): Promise { - this.setLoading(true) console.log(`____________________________________________GETTING FOLDER "${requestPath}"`); + // Returning cached folder + const { partialTree } = this.plugin.getPluginState() + const thisFolderIsCached = partialTree.find((item) => item.id === requestPath)?.cached + if (thisFolderIsCached) { + console.log("Folder was cached____________________________________________"); + this.plugin.setPluginState({ currentFolderId: requestPath, filterInput: '' }) + return + } + try { + this.setLoading(true) await this.#withAbort(async (signal) => { this.lastCheckbox = undefined @@ -242,7 +251,6 @@ export default class ProviderView extends View< - const { partialTree } = this.plugin.getPluginState() console.log({ partialTree }); if (partialTree.length === 0) { console.log("creating a new partial tree!"); @@ -330,6 +338,7 @@ export default class ProviderView extends View< * Fetches new folder */ getNextFolder(folder: FileInPartialTree): void { + console.log("____GET NEXT FOLDER____"); this.getFolder(folder.data.requestPath) this.lastCheckbox = undefined } From 4bc143f63e30b072bd5b25809fcfe0e78ca84d46 Mon Sep 17 00:00:00 2001 From: Evgenia Karunus Date: Mon, 1 Apr 2024 02:38:32 +0400 Subject: [PATCH 016/170] - enable shift-clicking --- packages/@uppy/provider-views/src/View.ts | 41 +++++++++++++++++------ 1 file changed, 30 insertions(+), 11 deletions(-) diff --git a/packages/@uppy/provider-views/src/View.ts b/packages/@uppy/provider-views/src/View.ts index 47a832eb8b..4f00b84b36 100644 --- a/packages/@uppy/provider-views/src/View.ts +++ b/packages/@uppy/provider-views/src/View.ts @@ -55,7 +55,7 @@ export default class View< isShiftKeyPressed: boolean - lastCheckbox: CompanionFile | undefined + lastCheckbox: string | undefined protected opts: O @@ -211,13 +211,9 @@ export default class View< e.stopPropagation() e.preventDefault() - const { partialTree } = this.plugin.getPluginState() + const { partialTree, currentFolderId } = this.plugin.getPluginState() const newPartialTree : PartialTree = JSON.parse(JSON.stringify(partialTree)) - const newStatus = ourItem.status === "checked" ? "unchecked" : "checked" - const ourItemInNewTree = newPartialTree.find((item) => item.id === ourItem.id)! - ourItemInNewTree.status = newStatus - // if newStatus is "checked" - percolate down "checked" // if newStatus is "unchecked" - percolate down "unchecked" const percolateDown = (currentFile: FileInPartialTree, status: StatusInPartialTree) => { @@ -227,9 +223,6 @@ export default class View< percolateDown(item, status) }) } - - percolateDown(ourItem, newStatus) - // we do something to all of its parents. const percolateUp = (currentFile: FileInPartialTree) => { const parentFolder = newPartialTree.find((item) => item.id === currentFile.parentId) @@ -239,7 +232,7 @@ export default class View< const parentsChildren = newPartialTree.filter((item) => item.parentId === parentFolder.id) as FileInPartialTree[] const areAllChildrenChecked = parentsChildren.every((item) => item.status === "checked") const areAllChildrenUnchecked = parentsChildren.every((item) => item.status === "unchecked") - + if (areAllChildrenChecked) { parentFolder.status = "checked" } else if (areAllChildrenUnchecked) { @@ -251,9 +244,35 @@ export default class View< percolateUp(parentFolder) } - percolateUp(ourItem) + // Shift-clicking selects a single consecutive list of items + // starting at the previous click. + const inThisFolder = this.filterItems(newPartialTree.filter((item) => item.parentId === currentFolderId)) + const prevIndex = inThisFolder.findIndex((item) => item.id === this.lastCheckbox) + if (prevIndex !== -1 && this.isShiftKeyPressed) { + const newIndex = inThisFolder.findIndex((item) => item.id === ourItem.id) + const toMarkAsChecked = (prevIndex < newIndex ? + inThisFolder.slice(prevIndex, newIndex + 1) + : inThisFolder.slice(newIndex, prevIndex + 1) + ).map((item) => item.id) + + const newlyCheckedItems = newPartialTree + .filter((item) => toMarkAsChecked.includes(item.id)) + + newlyCheckedItems.forEach((item) => item.status = "checked") + + newlyCheckedItems.forEach((item) => { + percolateDown(item, "checked") + }) + percolateUp(ourItem) + } else { + const ourItemInNewTree = newPartialTree.find((item) => item.id === ourItem.id)! + ourItemInNewTree.status = ourItem.status === "checked" ? "unchecked" : "checked" + percolateDown(ourItem, ourItemInNewTree.status) + percolateUp(ourItem) + } this.plugin.setPluginState({ partialTree: newPartialTree }) + this.lastCheckbox = ourItem.id } setLoading(loading: boolean | string): void { From f96f99fe68d2eaa5fdddd354e75a6ce66e072ae7 Mon Sep 17 00:00:00 2001 From: Evgenia Karunus Date: Mon, 1 Apr 2024 03:14:48 +0400 Subject: [PATCH 017/170] everywhere - get rid of unnecessary `.getNextFolder()` --- packages/@uppy/provider-views/src/Browser.tsx | 15 +++++++-------- .../src/ProviderView/ProviderView.tsx | 9 --------- 2 files changed, 7 insertions(+), 17 deletions(-) diff --git a/packages/@uppy/provider-views/src/Browser.tsx b/packages/@uppy/provider-views/src/Browser.tsx index fbc2d19b8c..4f977bb586 100644 --- a/packages/@uppy/provider-views/src/Browser.tsx +++ b/packages/@uppy/provider-views/src/Browser.tsx @@ -25,7 +25,7 @@ type ListItemProps = { showTitles: boolean i18n: I18n validateRestrictions: Uppy['validateRestrictions'] - getNextFolder?: (folder: FileInPartialTree) => void + getFolder: (folderId: string) => void f: FileInPartialTree } @@ -41,7 +41,7 @@ function ListItem( showTitles, i18n, validateRestrictions, - getNextFolder, + getFolder, f, } = props @@ -60,8 +60,7 @@ function ListItem( // TODO: when was this supposed to be true? isDisabled: false, isCheckboxDisabled: f.id === VIRTUAL_SHARED_DIR, - // getNextFolder always exists when f.isFolder is true - handleFolderClick: () => getNextFolder!(f), + handleFolderClick: () => getFolder(f.id), }) } const restrictionError = validateRestrictions(remoteFileObjToLocal(f.data), [ @@ -108,7 +107,7 @@ type BrowserProps = { searchOnInput: boolean searchInputLabel: string clearSearchLabel: string - getNextFolder?: (folder: any) => void + getFolder: (folder: any) => void cancel: () => void done: () => void noResultsLabel: string @@ -138,7 +137,7 @@ function Browser( searchOnInput, searchInputLabel, clearSearchLabel, - getNextFolder, + getFolder, cancel, done, noResultsLabel, @@ -211,7 +210,7 @@ function Browser( showTitles={showTitles} i18n={i18n} validateRestrictions={validateRestrictions} - getNextFolder={getNextFolder} + getFolder={getFolder} f={f} /> )} @@ -241,7 +240,7 @@ function Browser( showTitles={showTitles} i18n={i18n} validateRestrictions={validateRestrictions} - getNextFolder={getNextFolder} + getFolder={getFolder} f={f} /> ))} diff --git a/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx b/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx index f905b4e4c0..42aac2fa0a 100644 --- a/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx +++ b/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx @@ -98,7 +98,6 @@ export default class ProviderView extends View< this.filterQuery = this.filterQuery.bind(this) this.clearFilter = this.clearFilter.bind(this) this.getFolder = this.getFolder.bind(this) - this.getNextFolder = this.getNextFolder.bind(this) this.logout = this.logout.bind(this) this.handleAuth = this.handleAuth.bind(this) this.handleScroll = this.handleScroll.bind(this) @@ -332,14 +331,7 @@ export default class ProviderView extends View< } finally { this.setLoading(false) } - } - /** - * Fetches new folder - */ - getNextFolder(folder: FileInPartialTree): void { - console.log("____GET NEXT FOLDER____"); - this.getFolder(folder.data.requestPath) this.lastCheckbox = undefined } @@ -649,7 +641,6 @@ export default class ProviderView extends View< toggleCheckbox: this.toggleCheckbox.bind(this), recordShiftKeyPress, displayedPartialTree, - getNextFolder: this.getNextFolder, getFolder: this.getFolder, loadAllFiles: this.opts.loadAllFiles, From 296cae35ca230cd82fe9da78b407ecf004d16c57 Mon Sep 17 00:00:00 2001 From: Evgenia Karunus Date: Mon, 1 Apr 2024 03:22:18 +0400 Subject: [PATCH 018/170] everywhere - fixing types --- packages/@uppy/provider-views/src/Browser.tsx | 2 +- packages/@uppy/provider-views/src/Item/components/GridLi.tsx | 2 +- packages/@uppy/provider-views/src/Item/components/ListLi.tsx | 2 +- packages/@uppy/provider-views/src/Item/index.tsx | 4 ++-- .../src/SearchProviderView/SearchProviderView.tsx | 1 + 5 files changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/@uppy/provider-views/src/Browser.tsx b/packages/@uppy/provider-views/src/Browser.tsx index 4f977bb586..7654f13eaa 100644 --- a/packages/@uppy/provider-views/src/Browser.tsx +++ b/packages/@uppy/provider-views/src/Browser.tsx @@ -107,7 +107,7 @@ type BrowserProps = { searchOnInput: boolean searchInputLabel: string clearSearchLabel: string - getFolder: (folder: any) => void + getFolder: (folderId: any) => void cancel: () => void done: () => void noResultsLabel: string diff --git a/packages/@uppy/provider-views/src/Item/components/GridLi.tsx b/packages/@uppy/provider-views/src/Item/components/GridLi.tsx index 1e70db6663..41d79d15c7 100644 --- a/packages/@uppy/provider-views/src/Item/components/GridLi.tsx +++ b/packages/@uppy/provider-views/src/Item/components/GridLi.tsx @@ -9,7 +9,7 @@ type GridListItemProps = { className: string isDisabled: boolean restrictionError?: RestrictionError | null - status: StatusInPartialTree | null + status: StatusInPartialTree title?: string itemIconEl: any showTitles?: boolean diff --git a/packages/@uppy/provider-views/src/Item/components/ListLi.tsx b/packages/@uppy/provider-views/src/Item/components/ListLi.tsx index 3ead8ca714..8c872bde74 100644 --- a/packages/@uppy/provider-views/src/Item/components/ListLi.tsx +++ b/packages/@uppy/provider-views/src/Item/components/ListLi.tsx @@ -16,7 +16,7 @@ type ListItemProps = { isDisabled: boolean restrictionError?: RestrictionError | null isCheckboxDisabled: boolean - status: StatusInPartialTree | null + status: StatusInPartialTree toggleCheckbox: (event: Event) => void recordShiftKeyPress: (event: KeyboardEvent | MouseEvent) => void type: string diff --git a/packages/@uppy/provider-views/src/Item/index.tsx b/packages/@uppy/provider-views/src/Item/index.tsx index 1a5f64c264..041bb1688d 100644 --- a/packages/@uppy/provider-views/src/Item/index.tsx +++ b/packages/@uppy/provider-views/src/Item/index.tsx @@ -24,7 +24,7 @@ type ItemProps = { type: 'folder' | 'file' author?: CompanionFile['author'] getItemIcon: () => string - status: StatusInPartialTree | null + status: StatusInPartialTree isDisabled: boolean viewType: string } @@ -32,7 +32,7 @@ type ItemProps = { export default function Item( props: ItemProps, ): h.JSX.Element { - const { author, getItemIcon, status, isDisabled, viewType } = props + const { author, getItemIcon, isDisabled, viewType } = props const itemIconString = getItemIcon() const className = classNames( diff --git a/packages/@uppy/provider-views/src/SearchProviderView/SearchProviderView.tsx b/packages/@uppy/provider-views/src/SearchProviderView/SearchProviderView.tsx index ffe65460c9..7963eee11d 100644 --- a/packages/@uppy/provider-views/src/SearchProviderView/SearchProviderView.tsx +++ b/packages/@uppy/provider-views/src/SearchProviderView/SearchProviderView.tsx @@ -185,6 +185,7 @@ export default class SearchProviderView< handleScroll: this.handleScroll, done: this.donePicking, cancel: this.cancelPicking, + getFolder: () => {}, // For SearchFilterInput component showSearchFilter: targetViewOptions.showFilter, From 7123684fc7eee3ccd68def7ed3d8a2468c291667 Mon Sep 17 00:00:00 2001 From: Evgenia Karunus Date: Mon, 1 Apr 2024 03:41:38 +0400 Subject: [PATCH 019/170] - rename `requestPath` to `folderId` --- .../src/ProviderView/ProviderView.tsx | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx b/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx index 42aac2fa0a..f92155a4e1 100644 --- a/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx +++ b/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx @@ -210,14 +210,14 @@ export default class ProviderView extends View< * TODO rename to something better like selectFolder or navigateToFolder (breaking change?) * */ - async getFolder(requestPath?: string): Promise { - console.log(`____________________________________________GETTING FOLDER "${requestPath}"`); + async getFolder(folderId?: string): Promise { + console.log(`____________________________________________GETTING FOLDER "${folderId}"`); // Returning cached folder const { partialTree } = this.plugin.getPluginState() - const thisFolderIsCached = partialTree.find((item) => item.id === requestPath)?.cached + const thisFolderIsCached = partialTree.find((item) => item.id === folderId)?.cached if (thisFolderIsCached) { console.log("Folder was cached____________________________________________"); - this.plugin.setPluginState({ currentFolderId: requestPath, filterInput: '' }) + this.plugin.setPluginState({ currentFolderId: folderId, filterInput: '' }) return } @@ -226,7 +226,7 @@ export default class ProviderView extends View< await this.#withAbort(async (signal) => { this.lastCheckbox = undefined - this.nextPagePath = requestPath + this.nextPagePath = folderId let files: CompanionFile[] = [] let folders: CompanionFile[] = [] do { @@ -255,11 +255,11 @@ export default class ProviderView extends View< console.log("creating a new partial tree!"); const newPartialTree : PartialTree = [ ...folders.map((folder) => ({ - id: folder.requestPath, parentId: (requestPath || null), data: folder, + id: folder.requestPath, parentId: (folderId || null), data: folder, status: "unchecked", cached: false, })) as FileInPartialTree[], ...files.map((file) => ({ - id: file.requestPath, parentId: (requestPath || null), + id: file.requestPath, parentId: (folderId || null), status: "unchecked", cached: null, data: file })) as FileInPartialTree[] ] @@ -269,7 +269,7 @@ export default class ProviderView extends View< this.plugin.setPluginState({ partialTree: newPartialTree }) } else { console.log("appending to existing partial tree!"); - const clickedFolder : FileInPartialTree = partialTree.find((folder) => folder.id === requestPath)! + const clickedFolder : FileInPartialTree = partialTree.find((folder) => folder.id === folderId)! // If selected folder is already filled in, don't refill it (because that would make it lose deep state!) // Otherwise, cache the current folder! @@ -302,7 +302,7 @@ export default class ProviderView extends View< } } - this.plugin.setPluginState({ currentFolderId: (requestPath || null), filterInput: '' }) + this.plugin.setPluginState({ currentFolderId: (folderId || null), filterInput: '' }) }) From a5dc9e598e2f69d63ed27cc7d2f5c9318246d3fd Mon Sep 17 00:00:00 2001 From: Evgenia Karunus Date: Tue, 2 Apr 2024 03:40:51 +0400 Subject: [PATCH 020/170] all providers - get rid of `.onFirstRender()` --- packages/@uppy/box/src/Box.tsx | 11 +++-------- packages/@uppy/dropbox/src/Dropbox.tsx | 11 +++-------- packages/@uppy/facebook/src/Facebook.tsx | 15 ++++++--------- packages/@uppy/google-drive/src/GoogleDrive.tsx | 11 +++-------- packages/@uppy/instagram/src/Instagram.tsx | 11 +++-------- packages/@uppy/onedrive/src/OneDrive.tsx | 11 +++-------- packages/@uppy/zoom/src/Zoom.tsx | 11 +++-------- 7 files changed, 24 insertions(+), 57 deletions(-) diff --git a/packages/@uppy/box/src/Box.tsx b/packages/@uppy/box/src/Box.tsx index 914684106b..aa2cb2301c 100644 --- a/packages/@uppy/box/src/Box.tsx +++ b/packages/@uppy/box/src/Box.tsx @@ -35,6 +35,8 @@ export default class Box extends UIPlugin< files: UppyFile[] + rootFolderId: string | null + constructor(uppy: Uppy, opts: BoxOptions) { super(uppy, opts) this.id = this.opts.id || 'Box' @@ -56,6 +58,7 @@ export default class Box extends UIPlugin< ) + this.rootFolderId = null this.opts.companionAllowedHosts = getAllowedHosts( this.opts.companionAllowedHosts, @@ -76,7 +79,6 @@ export default class Box extends UIPlugin< this.i18nInit() this.title = this.i18n('pluginNameBox') - this.onFirstRender = this.onFirstRender.bind(this) this.render = this.render.bind(this) } @@ -97,13 +99,6 @@ export default class Box extends UIPlugin< this.unmount() } - async onFirstRender(): Promise { - await Promise.all([ - this.provider.fetchPreAuthToken(), - this.view.getFolder(), - ]) - } - render(state: unknown): ComponentChild { return this.view.render(state) } diff --git a/packages/@uppy/dropbox/src/Dropbox.tsx b/packages/@uppy/dropbox/src/Dropbox.tsx index 1e5c2cca7c..6bdb9a6f25 100644 --- a/packages/@uppy/dropbox/src/Dropbox.tsx +++ b/packages/@uppy/dropbox/src/Dropbox.tsx @@ -35,6 +35,8 @@ export default class Dropbox extends UIPlugin< files: UppyFile[] + rootFolderId: string | null + constructor(uppy: Uppy, opts: DropboxOptions) { super(uppy, opts) this.id = this.opts.id || 'Dropbox' @@ -57,6 +59,7 @@ export default class Dropbox extends UIPlugin< /> ) + this.rootFolderId = null this.opts.companionAllowedHosts = getAllowedHosts( this.opts.companionAllowedHosts, @@ -77,7 +80,6 @@ export default class Dropbox extends UIPlugin< this.i18nInit() this.title = this.opts.title || this.i18n('pluginNameDropbox') - this.onFirstRender = this.onFirstRender.bind(this) this.render = this.render.bind(this) } @@ -98,13 +100,6 @@ export default class Dropbox extends UIPlugin< this.unmount() } - async onFirstRender(): Promise { - await Promise.all([ - this.provider.fetchPreAuthToken(), - this.view.getFolder(), - ]) - } - render(state: unknown): ComponentChild { return this.view.render(state) } diff --git a/packages/@uppy/facebook/src/Facebook.tsx b/packages/@uppy/facebook/src/Facebook.tsx index fa870254cf..e4b888d276 100644 --- a/packages/@uppy/facebook/src/Facebook.tsx +++ b/packages/@uppy/facebook/src/Facebook.tsx @@ -35,6 +35,8 @@ export default class Facebook extends UIPlugin< files: UppyFile[] + rootFolderId: string | null + constructor(uppy: Uppy, opts: FacebookOptions) { super(uppy, opts) this.id = this.opts.id || 'Facebook' @@ -61,6 +63,7 @@ export default class Facebook extends UIPlugin< ) + this.rootFolderId = null this.opts.companionAllowedHosts = getAllowedHosts( this.opts.companionAllowedHosts, @@ -81,7 +84,6 @@ export default class Facebook extends UIPlugin< this.i18nInit() this.title = this.i18n('pluginNameFacebook') - this.onFirstRender = this.onFirstRender.bind(this) this.render = this.render.bind(this) } @@ -101,13 +103,6 @@ export default class Facebook extends UIPlugin< this.unmount() } - async onFirstRender(): Promise { - await Promise.all([ - this.provider.fetchPreAuthToken(), - this.view.getFolder(), - ]) - } - render(state: unknown): ComponentChild { const viewOptions: { viewType?: string @@ -115,7 +110,9 @@ export default class Facebook extends UIPlugin< showTitles?: boolean } = {} const { partialTree } = this.getPluginState() - const nOfFolders = partialTree.filter((item) => item.data.type === "folder").length + const nOfFolders = partialTree.filter( + (item) => item.data.type === 'folder', + ).length if (nOfFolders === 0) { viewOptions.viewType = 'grid' viewOptions.showFilter = false diff --git a/packages/@uppy/google-drive/src/GoogleDrive.tsx b/packages/@uppy/google-drive/src/GoogleDrive.tsx index 1ae6a231b0..f4de6c7cd7 100644 --- a/packages/@uppy/google-drive/src/GoogleDrive.tsx +++ b/packages/@uppy/google-drive/src/GoogleDrive.tsx @@ -34,6 +34,8 @@ export default class GoogleDrive< files: UppyFile[] + rootFolderId: string | null + constructor(uppy: Uppy, opts: GoogleDriveOptions) { super(uppy, opts) this.type = 'acquirer' @@ -76,6 +78,7 @@ export default class GoogleDrive< ) + this.rootFolderId = 'root' this.opts.companionAllowedHosts = getAllowedHosts( this.opts.companionAllowedHosts, @@ -96,7 +99,6 @@ export default class GoogleDrive< this.i18nInit() this.title = this.i18n('pluginNameGoogleDrive') - this.onFirstRender = this.onFirstRender.bind(this) this.render = this.render.bind(this) } @@ -117,13 +119,6 @@ export default class GoogleDrive< this.unmount() } - async onFirstRender(): Promise { - await Promise.all([ - this.provider.fetchPreAuthToken(), - this.view.getFolder('root'), - ]) - } - render(state: unknown): ComponentChild { return this.view.render(state) } diff --git a/packages/@uppy/instagram/src/Instagram.tsx b/packages/@uppy/instagram/src/Instagram.tsx index 7a215126d7..8262bf4f59 100644 --- a/packages/@uppy/instagram/src/Instagram.tsx +++ b/packages/@uppy/instagram/src/Instagram.tsx @@ -35,6 +35,8 @@ export default class Instagram extends UIPlugin< files: UppyFile[] + rootFolderId: string | null + constructor(uppy: Uppy, opts: InstagramOptions) { super(uppy, opts) this.type = 'acquirer' @@ -70,6 +72,7 @@ export default class Instagram extends UIPlugin< ) + this.rootFolderId = 'recent' this.defaultLocale = locale @@ -90,7 +93,6 @@ export default class Instagram extends UIPlugin< supportsRefreshToken: false, }) - this.onFirstRender = this.onFirstRender.bind(this) this.render = this.render.bind(this) } @@ -114,13 +116,6 @@ export default class Instagram extends UIPlugin< this.unmount() } - async onFirstRender(): Promise { - await Promise.all([ - this.provider.fetchPreAuthToken(), - this.view.getFolder('recent'), - ]) - } - render(state: unknown): ComponentChild { return this.view.render(state) } diff --git a/packages/@uppy/onedrive/src/OneDrive.tsx b/packages/@uppy/onedrive/src/OneDrive.tsx index 6d7c20c6d4..0e0dc28a47 100644 --- a/packages/@uppy/onedrive/src/OneDrive.tsx +++ b/packages/@uppy/onedrive/src/OneDrive.tsx @@ -35,6 +35,8 @@ export default class OneDrive extends UIPlugin< files: UppyFile[] + rootFolderId: string | null + constructor(uppy: Uppy, opts: OneDriveOptions) { super(uppy, opts) this.type = 'acquirer' @@ -69,6 +71,7 @@ export default class OneDrive extends UIPlugin< ) + this.rootFolderId = null this.opts.companionAllowedHosts = getAllowedHosts( this.opts.companionAllowedHosts, @@ -89,7 +92,6 @@ export default class OneDrive extends UIPlugin< this.i18nInit() this.title = this.i18n('pluginNameOneDrive') - this.onFirstRender = this.onFirstRender.bind(this) this.render = this.render.bind(this) } @@ -110,13 +112,6 @@ export default class OneDrive extends UIPlugin< this.unmount() } - async onFirstRender(): Promise { - await Promise.all([ - this.provider.fetchPreAuthToken(), - this.view.getFolder(), - ]) - } - render(state: unknown): ComponentChild { return this.view.render(state) } diff --git a/packages/@uppy/zoom/src/Zoom.tsx b/packages/@uppy/zoom/src/Zoom.tsx index 3bc576fa40..01ff9de0b5 100644 --- a/packages/@uppy/zoom/src/Zoom.tsx +++ b/packages/@uppy/zoom/src/Zoom.tsx @@ -35,6 +35,8 @@ export default class Zoom extends UIPlugin< files: UppyFile[] + rootFolderId: string | null + constructor(uppy: Uppy, opts: ZoomOptions) { super(uppy, opts) this.type = 'acquirer' @@ -56,6 +58,7 @@ export default class Zoom extends UIPlugin< /> ) + this.rootFolderId = null this.opts.companionAllowedHosts = getAllowedHosts( this.opts.companionAllowedHosts, @@ -76,7 +79,6 @@ export default class Zoom extends UIPlugin< this.i18nInit() this.title = this.i18n('pluginNameZoom') - this.onFirstRender = this.onFirstRender.bind(this) this.render = this.render.bind(this) } @@ -96,13 +98,6 @@ export default class Zoom extends UIPlugin< this.unmount() } - async onFirstRender(): Promise { - await Promise.all([ - this.provider.fetchPreAuthToken(), - this.view.getFolder(), - ]) - } - render(state: unknown): ComponentChild { return this.view.render(state) } From 60a9c1eaab79563c84aa7e63bd35f07201a8523d Mon Sep 17 00:00:00 2001 From: Evgenia Karunus Date: Tue, 2 Apr 2024 04:05:41 +0400 Subject: [PATCH 021/170] provider views - get rid of `.onFirstRender()` --- packages/@uppy/core/src/Uppy.ts | 2 +- packages/@uppy/provider-views/README.md | 7 ------- .../src/ProviderView/ProviderView.tsx | 15 ++++++++++----- packages/@uppy/provider-views/src/View.ts | 6 ------ .../@uppy/utils/src/CompanionClientProvider.ts | 1 + 5 files changed, 12 insertions(+), 19 deletions(-) diff --git a/packages/@uppy/core/src/Uppy.ts b/packages/@uppy/core/src/Uppy.ts index f6b120f0ce..ed615c2325 100644 --- a/packages/@uppy/core/src/Uppy.ts +++ b/packages/@uppy/core/src/Uppy.ts @@ -96,8 +96,8 @@ export type UnknownProviderPlugin< M extends Meta, B extends Body, > = UnknownPlugin & { - onFirstRender: () => void title: string + rootFolderId: string | null files: UppyFile[] icon: () => JSX.Element provider: CompanionClientProvider diff --git a/packages/@uppy/provider-views/README.md b/packages/@uppy/provider-views/README.md index c3be4986bb..80d5d1ac6e 100644 --- a/packages/@uppy/provider-views/README.md +++ b/packages/@uppy/provider-views/README.md @@ -23,13 +23,6 @@ class GoogleDrive extends UIPlugin { // snip } - onFirstRender () { - return Promise.all([ - this.provider.fetchPreAuthToken(), - this.view.getFolder('root'), - ]) - } - render (state) { return this.view.render(state) } diff --git a/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx b/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx index f92155a4e1..a0681b8674 100644 --- a/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx +++ b/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx @@ -210,11 +210,11 @@ export default class ProviderView extends View< * TODO rename to something better like selectFolder or navigateToFolder (breaking change?) * */ - async getFolder(folderId?: string): Promise { + async getFolder(folderId: string | null): Promise { console.log(`____________________________________________GETTING FOLDER "${folderId}"`); // Returning cached folder const { partialTree } = this.plugin.getPluginState() - const thisFolderIsCached = partialTree.find((item) => item.id === folderId)?.cached + const thisFolderIsCached = partialTree.find((folder) => folder.id === folderId)?.cached if (thisFolderIsCached) { console.log("Folder was cached____________________________________________"); this.plugin.setPluginState({ currentFolderId: folderId, filterInput: '' }) @@ -226,7 +226,7 @@ export default class ProviderView extends View< await this.#withAbort(async (signal) => { this.lastCheckbox = undefined - this.nextPagePath = folderId + this.nextPagePath = folderId || undefined let files: CompanionFile[] = [] let folders: CompanionFile[] = [] do { @@ -386,7 +386,10 @@ export default class ProviderView extends View< this.setLoading(true) await this.provider.login({ authFormData, signal }) this.plugin.setPluginState({ authenticated: true }) - this.preFirstRender() + await Promise.all([ + this.provider.fetchPreAuthToken(), + this.getFolder(this.plugin.rootFolderId), + ]) }) } catch (err) { if (err.name === 'UserFacingApiError') { @@ -615,7 +618,9 @@ export default class ProviderView extends View< const { i18n } = this.plugin.uppy if (!didFirstRender) { - this.preFirstRender() + this.plugin.setPluginState({ didFirstRender: true }) + this.provider.fetchPreAuthToken() + this.getFolder(this.plugin.rootFolderId) } const targetViewOptions = { ...this.opts, ...viewOptions } diff --git a/packages/@uppy/provider-views/src/View.ts b/packages/@uppy/provider-views/src/View.ts index 4f00b84b36..7e09d01c9b 100644 --- a/packages/@uppy/provider-views/src/View.ts +++ b/packages/@uppy/provider-views/src/View.ts @@ -66,17 +66,11 @@ export default class View< this.isHandlingScroll = false - this.preFirstRender = this.preFirstRender.bind(this) this.handleError = this.handleError.bind(this) this.clearSelection = this.clearSelection.bind(this) this.cancelPicking = this.cancelPicking.bind(this) } - preFirstRender(): void { - this.plugin.setPluginState({ didFirstRender: true }) - this.plugin.onFirstRender() - } - shouldHandleScroll(event: Event): boolean { const { scrollHeight, scrollTop, offsetHeight } = event.target as HTMLElement diff --git a/packages/@uppy/utils/src/CompanionClientProvider.ts b/packages/@uppy/utils/src/CompanionClientProvider.ts index a7f2cba108..e4bd94c8b3 100644 --- a/packages/@uppy/utils/src/CompanionClientProvider.ts +++ b/packages/@uppy/utils/src/CompanionClientProvider.ts @@ -23,6 +23,7 @@ export interface CompanionClientProvider { provider: string login(options?: RequestOptions): Promise logout(options?: RequestOptions): Promise + fetchPreAuthToken(): Promise list( directory: string | undefined, options: RequestOptions, From 205bc45448314a4aff7659af937fe80f5bdeebd8 Mon Sep 17 00:00:00 2001 From: Evgenia Karunus Date: Tue, 2 Apr 2024 04:37:44 +0400 Subject: [PATCH 022/170] - make the root folder cacheable too --- .../src/ProviderView/ProviderView.tsx | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx b/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx index a0681b8674..b806655138 100644 --- a/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx +++ b/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx @@ -88,6 +88,8 @@ export default class ProviderView extends View< nextPagePath: string | undefined + isRootFolderFetched: boolean = false; + constructor( plugin: UnknownProviderPlugin, opts: ProviderViewOptions, @@ -214,7 +216,9 @@ export default class ProviderView extends View< console.log(`____________________________________________GETTING FOLDER "${folderId}"`); // Returning cached folder const { partialTree } = this.plugin.getPluginState() - const thisFolderIsCached = partialTree.find((folder) => folder.id === folderId)?.cached + const thisFolderIsCached = + partialTree.find((folder) => folder.id === folderId)?.cached || + (folderId === this.plugin.rootFolderId && this.isRootFolderFetched) if (thisFolderIsCached) { console.log("Folder was cached____________________________________________"); this.plugin.setPluginState({ currentFolderId: folderId, filterInput: '' }) @@ -251,7 +255,7 @@ export default class ProviderView extends View< console.log({ partialTree }); - if (partialTree.length === 0) { + if (!this.isRootFolderFetched) { console.log("creating a new partial tree!"); const newPartialTree : PartialTree = [ ...folders.map((folder) => ({ @@ -266,7 +270,8 @@ export default class ProviderView extends View< console.log({ newPartialTree }); - this.plugin.setPluginState({ partialTree: newPartialTree }) + this.isRootFolderFetched = true + this.plugin.setPluginState({ partialTree: newPartialTree, currentFolderId: folderId, filterInput: '' }) } else { console.log("appending to existing partial tree!"); const clickedFolder : FileInPartialTree = partialTree.find((folder) => folder.id === folderId)! @@ -297,12 +302,13 @@ export default class ProviderView extends View< partialTree: [ ...partialTreeWithUpdatedClickedFolder, ...clickedFolderContents - ] + ], + currentFolderId: folderId, + filterInput: '' }) } } - this.plugin.setPluginState({ currentFolderId: (folderId || null), filterInput: '' }) }) From 3cc111eca62b80209a0e31caaea3b789f75fadba Mon Sep 17 00:00:00 2001 From: Evgenia Karunus Date: Tue, 2 Apr 2024 05:49:07 +0400 Subject: [PATCH 023/170] TEMP - setup for working with FOLDERS + LAZY_LOADING --- packages/@uppy/companion/src/server/provider/drive/index.js | 2 +- packages/@uppy/google-drive/src/GoogleDrive.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/@uppy/companion/src/server/provider/drive/index.js b/packages/@uppy/companion/src/server/provider/drive/index.js index 384a86b4ed..93386d8ab4 100644 --- a/packages/@uppy/companion/src/server/provider/drive/index.js +++ b/packages/@uppy/companion/src/server/provider/drive/index.js @@ -98,7 +98,7 @@ class Drive extends Provider { q, // We can only do a page size of 1000 because we do not request permissions in DRIVE_FILES_FIELDS. // Otherwise we are limited to 100. Instead we get the user info from `this.user()` - pageSize: 1000, + pageSize: 5, orderBy: 'folder,name', includeItemsFromAllDrives: true, supportsAllDrives: true, diff --git a/packages/@uppy/google-drive/src/GoogleDrive.tsx b/packages/@uppy/google-drive/src/GoogleDrive.tsx index f4de6c7cd7..c1b8e16e74 100644 --- a/packages/@uppy/google-drive/src/GoogleDrive.tsx +++ b/packages/@uppy/google-drive/src/GoogleDrive.tsx @@ -105,7 +105,7 @@ export default class GoogleDrive< install(): void { this.view = new DriveProviderViews(this, { provider: this.provider, - loadAllFiles: true, + loadAllFiles: false, }) const { target } = this.opts From ab58d89e4b5f85afe43bf89ff3f9c9fc23441e9e Mon Sep 17 00:00:00 2001 From: Evgenia Karunus Date: Wed, 3 Apr 2024 05:19:29 +0400 Subject: [PATCH 024/170] - get rid of `.#listFilesAndFolders` --- .../src/ProviderView/ProviderView.tsx | 102 +++++++----------- 1 file changed, 41 insertions(+), 61 deletions(-) diff --git a/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx b/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx index b806655138..6c71ded878 100644 --- a/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx +++ b/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx @@ -159,7 +159,7 @@ export default class ProviderView extends View< absDirPath, signal, }: { - requestPath?: string + requestPath: string | null absDirPath: string signal: AbortSignal }) { @@ -167,7 +167,7 @@ export default class ProviderView extends View< username: string nextPagePath: string items: CompanionFile[] - }>(requestPath, { signal }) + }>(requestPath || undefined, { signal }) this.username = username || this.username return { @@ -179,40 +179,13 @@ export default class ProviderView extends View< } } - async #listFilesAndFolders({ breadcrumbs, signal }: { - breadcrumbs: FileInPartialTree[], - signal: AbortSignal - }) { - const absDirPath = formatBreadcrumbs(breadcrumbs) - - const { items, nextPagePath } = await this.#list({ - requestPath: this.nextPagePath, - absDirPath, - signal, - }) - - this.nextPagePath = nextPagePath - - const files: CompanionFile[] = [] - const folders: CompanionFile[] = [] - - items.forEach((item) => { - if (item.isFolder) { - folders.push(item) - } else { - files.push(item) - } - }) - - return { files, folders } - } - /** * 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?) * */ async getFolder(folderId: string | null): Promise { + this.lastCheckbox = undefined console.log(`____________________________________________GETTING FOLDER "${folderId}"`); // Returning cached folder const { partialTree } = this.plugin.getPluginState() @@ -228,28 +201,24 @@ export default class ProviderView extends View< try { this.setLoading(true) await this.#withAbort(async (signal) => { - this.lastCheckbox = undefined - this.nextPagePath = folderId || undefined - let files: CompanionFile[] = [] - let folders: CompanionFile[] = [] + let currentPagePath = folderId + let currentItems: CompanionFile[] = [] do { - const { files: newFiles, folders: newFolders } = await this.#listFilesAndFolders({ - breadcrumbs: this.getBreadcrumbs(), signal, + const { items, nextPagePath } = await this.#list({ + requestPath: currentPagePath, + absDirPath: formatBreadcrumbs(this.getBreadcrumbs()), + signal }) + currentPagePath = nextPagePath + currentItems = currentItems.concat(items) + this.setLoading(this.plugin.uppy.i18n('loadedXFiles', { numFiles: items.length })) + } while (this.opts.loadAllFiles && currentPagePath) - 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) - - - - - - + let newFolders = currentItems.filter((i) => i.isFolder === true) + let newFiles = currentItems.filter((i) => i.isFolder === false) + console.log({ newFolders, newFiles}); @@ -257,13 +226,14 @@ export default class ProviderView extends View< console.log({ partialTree }); if (!this.isRootFolderFetched) { console.log("creating a new partial tree!"); + const newPartialTree : PartialTree = [ - ...folders.map((folder) => ({ - id: folder.requestPath, parentId: (folderId || null), data: folder, + ...newFolders.map((folder) => ({ + id: folder.requestPath, parentId: folderId, data: folder, status: "unchecked", cached: false, })) as FileInPartialTree[], - ...files.map((file) => ({ - id: file.requestPath, parentId: (folderId || null), + ...newFiles.map((file) => ({ + id: file.requestPath, parentId: folderId, status: "unchecked", cached: null, data: file })) as FileInPartialTree[] ] @@ -280,11 +250,11 @@ export default class ProviderView extends View< // Otherwise, cache the current folder! if (clickedFolder && !clickedFolder.cached) { const clickedFolderContents : FileInPartialTree[] = [ - ...folders.map((folder) => ({ + ...newFolders.map((folder) => ({ id: folder.requestPath, parentId: clickedFolder.id, data: folder, status: clickedFolder.status, cached: false, })), - ...files.map((file) => ({ + ...newFiles.map((file) => ({ id: file.requestPath, parentId: clickedFolder.id, data: file, status: clickedFolder.status, cached: null, })), @@ -298,11 +268,15 @@ export default class ProviderView extends View< folder ) + const newPartialTree = [ + ...partialTreeWithUpdatedClickedFolder, + ...clickedFolderContents + ] + + console.log({ newPartialTree, folderId }); + this.plugin.setPluginState({ - partialTree: [ - ...partialTreeWithUpdatedClickedFolder, - ...clickedFolderContents - ], + partialTree: newPartialTree, currentFolderId: folderId, filterInput: '' }) @@ -422,17 +396,23 @@ export default class ProviderView extends View< await this.#withAbort(async (signal) => { const { partialTree, currentFolderId } = this.plugin.getPluginState() - const { files, folders } = await this.#listFilesAndFolders({ - breadcrumbs: this.getBreadcrumbs(), signal, + const { items, nextPagePath } = await this.#list({ + requestPath: this.nextPagePath, + absDirPath: formatBreadcrumbs(this.getBreadcrumbs()), + signal }) + let newFolders = items.filter((i) => i.isFolder === true) + let newFiles = items.filter((i) => i.isFolder === false) + + // TODO nextPagePath shoud be inserted into .partialTree here const newPartialTree = [ ...partialTree, - ...folders.map((folder) => ({ + ...newFolders.map((folder) => ({ id: folder.requestPath, parentId: currentFolderId, data: folder, status: "unchecked", cached: false, })) as FileInPartialTree[], - ...files.map((file) => ({ + ...newFiles.map((file) => ({ id: file.requestPath, parentId: currentFolderId, data: file, status: "unchecked", cached: null })) as FileInPartialTree[] From e9d5ee3c3e7cb050f359486989b2a930c7e763b4 Mon Sep 17 00:00:00 2001 From: Evgenia Karunus Date: Wed, 3 Apr 2024 05:41:13 +0400 Subject: [PATCH 025/170] - make `this.nextPagePath` per-folder --- packages/@uppy/core/src/Uppy.ts | 7 ++-- .../src/ProviderView/ProviderView.tsx | 36 ++++++++++--------- 2 files changed, 24 insertions(+), 19 deletions(-) diff --git a/packages/@uppy/core/src/Uppy.ts b/packages/@uppy/core/src/Uppy.ts index ed615c2325..70df06a068 100644 --- a/packages/@uppy/core/src/Uppy.ts +++ b/packages/@uppy/core/src/Uppy.ts @@ -65,8 +65,11 @@ export type StatusInPartialTree = "checked" | "unchecked" | "partial" export type FileInPartialTree = { id: string - // really .cached is always a `boolean` for "folder"s, and `null` for "file"s - cached: boolean | null + + // this is only for folders + cached?: boolean + nextPagePath?: string + status: StatusInPartialTree parentId: string | null data: CompanionFile diff --git a/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx b/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx index 6c71ded878..eb3d1fe7c1 100644 --- a/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx +++ b/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx @@ -218,23 +218,17 @@ export default class ProviderView extends View< let newFolders = currentItems.filter((i) => i.isFolder === true) let newFiles = currentItems.filter((i) => i.isFolder === false) - console.log({ newFolders, newFiles}); - - - - - console.log({ partialTree }); if (!this.isRootFolderFetched) { console.log("creating a new partial tree!"); const newPartialTree : PartialTree = [ ...newFolders.map((folder) => ({ id: folder.requestPath, parentId: folderId, data: folder, - status: "unchecked", cached: false, + status: "unchecked", cached: false, nextPagePath: folder.requestPath })) as FileInPartialTree[], ...newFiles.map((file) => ({ id: file.requestPath, parentId: folderId, - status: "unchecked", cached: null, data: file + status: "unchecked", data: file })) as FileInPartialTree[] ] @@ -256,12 +250,12 @@ export default class ProviderView extends View< })), ...newFiles.map((file) => ({ id: file.requestPath, parentId: clickedFolder.id, data: file, - status: clickedFolder.status, cached: null, + status: clickedFolder.status })), ] // just doing `clickedFolder.cached = true` in a non-mutating way - const updatedClickedFolder : FileInPartialTree = { ...clickedFolder, cached: true } + const updatedClickedFolder : FileInPartialTree = { ...clickedFolder, cached: true, nextPagePath: currentPagePath } const partialTreeWithUpdatedClickedFolder = partialTree.map((folder) => folder.id === updatedClickedFolder.id ? updatedClickedFolder : @@ -388,16 +382,16 @@ export default class ProviderView extends View< } async handleScroll(event: Event): Promise { - console.log("handleScrolll"); - if (this.shouldHandleScroll(event) && this.nextPagePath) { + const { partialTree, currentFolderId } = this.plugin.getPluginState() + const currentFolder = partialTree.find((i) => i.id === currentFolderId)! + if (this.shouldHandleScroll(event) && currentFolder.nextPagePath) { this.isHandlingScroll = true try { await this.#withAbort(async (signal) => { - const { partialTree, currentFolderId } = this.plugin.getPluginState() const { items, nextPagePath } = await this.#list({ - requestPath: this.nextPagePath, + requestPath: currentFolder.nextPagePath!, absDirPath: formatBreadcrumbs(this.getBreadcrumbs()), signal }) @@ -406,15 +400,23 @@ export default class ProviderView extends View< // TODO nextPagePath shoud be inserted into .partialTree here + // just doing `clickedFolder.cached = true` in a non-mutating way + const updatedClickedFolder : FileInPartialTree = { ...currentFolder, nextPagePath } + const partialTreeWithUpdatedCurrentFolder = partialTree.map((folder) => + folder.id === updatedClickedFolder.id ? + updatedClickedFolder : + folder + ) + const newPartialTree = [ - ...partialTree, + ...partialTreeWithUpdatedCurrentFolder, ...newFolders.map((folder) => ({ id: folder.requestPath, parentId: currentFolderId, data: folder, - status: "unchecked", cached: false, + status: "unchecked", cached: false, nextPagePath: folder.requestPath })) as FileInPartialTree[], ...newFiles.map((file) => ({ id: file.requestPath, parentId: currentFolderId, data: file, - status: "unchecked", cached: null + status: "unchecked" })) as FileInPartialTree[] ] From 750a42271d3a621bf729aa01bc49b16a94465128 Mon Sep 17 00:00:00 2001 From: Evgenia Karunus Date: Wed, 3 Apr 2024 08:31:30 +0400 Subject: [PATCH 026/170] everywhere - more refined types --- packages/@uppy/core/src/Uppy.ts | 36 +- .../@uppy/provider-views/src/Breadcrumbs.tsx | 51 +-- packages/@uppy/provider-views/src/Browser.tsx | 16 +- .../@uppy/provider-views/src/Item/index.tsx | 4 +- .../src/ProviderView/ProviderView.tsx | 417 +++++++++--------- packages/@uppy/provider-views/src/View.ts | 43 +- 6 files changed, 285 insertions(+), 282 deletions(-) diff --git a/packages/@uppy/core/src/Uppy.ts b/packages/@uppy/core/src/Uppy.ts index 70df06a068..0baa54194a 100644 --- a/packages/@uppy/core/src/Uppy.ts +++ b/packages/@uppy/core/src/Uppy.ts @@ -61,21 +61,41 @@ export type UnknownPlugin< PluginState extends Record = Record, > = BasePlugin -export type StatusInPartialTree = "checked" | "unchecked" | "partial" +// ids are always `string`s, except the root folder's id can be `null` +export type PartialTreeId = string | null -export type FileInPartialTree = { +export type PartialTreeFile = { + type: 'file' id: string - // this is only for folders - cached?: boolean - nextPagePath?: string + status: 'checked' | 'unchecked' + parentId: PartialTreeId + data: CompanionFile +} + +export type PartialTreeFolder = PartialTreeFolderNode | PartialTreeFolderRoot + +export type PartialTreeFolderNode = { + type: 'folder' + id: string - status: StatusInPartialTree - parentId: string | null + cached: boolean + nextPagePath: PartialTreeId + + status: 'checked' | 'unchecked' | 'partial' + parentId: PartialTreeId data: CompanionFile } -export type PartialTree = FileInPartialTree[] +export type PartialTreeFolderRoot = { + type: 'root' + id: PartialTreeId + + cached: boolean + nextPagePath: PartialTreeId +} + +export type PartialTree = (PartialTreeFile | PartialTreeFolder)[] export type UnknownProviderPluginState = { authenticated: boolean | undefined diff --git a/packages/@uppy/provider-views/src/Breadcrumbs.tsx b/packages/@uppy/provider-views/src/Breadcrumbs.tsx index 967131f675..078dbf5e81 100644 --- a/packages/@uppy/provider-views/src/Breadcrumbs.tsx +++ b/packages/@uppy/provider-views/src/Breadcrumbs.tsx @@ -1,36 +1,13 @@ -import type { FileInPartialTree, UnknownProviderPluginState } from '@uppy/core/lib/Uppy' +import type { PartialTreeFolder } from '@uppy/core/lib/Uppy' import { h, Fragment } from 'preact' import type { Body, Meta } from '@uppy/utils/lib/UppyFile' import type ProviderView from './ProviderView' -type BreadcrumbProps = { - getFolder: () => void - title: string - isFirst?: boolean -} - -const Breadcrumb = (props: BreadcrumbProps) => { - const { getFolder, title, isFirst } = props - - return ( - - {!isFirst ? ' / ' : ''} - - - ) -} - type BreadcrumbsProps = { getFolder: ProviderView['getFolder'] title: string breadcrumbsIcon: JSX.Element - breadcrumbs: FileInPartialTree[] + breadcrumbs: PartialTreeFolder[] } export default function Breadcrumbs( @@ -41,18 +18,18 @@ export default function Breadcrumbs( return (
        {breadcrumbsIcon}
        - getFolder("root")} - title={title} - isFirst - /> - {breadcrumbs.map((directory, i) => ( - getFolder(directory.data.requestPath)} - title={directory.data.name} - /> + {breadcrumbs.map((folder, index) => ( + + + {breadcrumbs.length === index + 1 ? '' : ' / '} + ))}
        ) diff --git a/packages/@uppy/provider-views/src/Browser.tsx b/packages/@uppy/provider-views/src/Browser.tsx index 7654f13eaa..125b374cb0 100644 --- a/packages/@uppy/provider-views/src/Browser.tsx +++ b/packages/@uppy/provider-views/src/Browser.tsx @@ -12,7 +12,7 @@ import type Uppy from '@uppy/core' import SearchFilterInput from './SearchFilterInput.tsx' import FooterActions from './FooterActions.tsx' import Item from './Item/index.tsx' -import type { FileInPartialTree, PartialTree } from '@uppy/core/lib/Uppy.ts' +import type { PartialTree, PartialTreeFile, PartialTreeFolderNode, PartialTreeId } from '@uppy/core/lib/Uppy.ts' const VIRTUAL_SHARED_DIR = 'shared-with-me' @@ -20,13 +20,13 @@ type ListItemProps = { currentSelection: any[] uppyFiles: UppyFile[] viewType: string - toggleCheckbox: (event: Event, file: FileInPartialTree) => void + toggleCheckbox: (event: Event, file: (PartialTreeFile | PartialTreeFolderNode)) => void recordShiftKeyPress: (event: KeyboardEvent | MouseEvent) => void showTitles: boolean i18n: I18n validateRestrictions: Uppy['validateRestrictions'] - getFolder: (folderId: string) => void - f: FileInPartialTree + getFolder: (folderId: PartialTreeId) => void + f: (PartialTreeFile | PartialTreeFolderNode) } function ListItem( @@ -87,13 +87,13 @@ function ListItem( } type BrowserProps = { - displayedPartialTree: PartialTree, - currentSelection: FileInPartialTree[], + displayedPartialTree: (PartialTreeFile | PartialTreeFolderNode)[], + currentSelection: (PartialTreeFile | PartialTreeFolderNode)[], uppyFiles: UppyFile[] viewType: string headerComponent?: JSX.Element showBreadcrumbs: boolean - toggleCheckbox: (event: Event, file: FileInPartialTree) => void + toggleCheckbox: (event: Event, file: PartialTreeFile | PartialTreeFolderNode) => void recordShiftKeyPress: (event: KeyboardEvent | MouseEvent) => void handleScroll: (event: Event) => Promise showTitles: boolean @@ -200,7 +200,7 @@ function Browser(
          ( + renderRow={(f: PartialTreeFile | PartialTreeFolderNode) => ( = { showTitles: boolean @@ -24,7 +23,7 @@ type ItemProps = { type: 'folder' | 'file' author?: CompanionFile['author'] getItemIcon: () => string - status: StatusInPartialTree + status: 'checked' | 'unchecked' | 'partial' isDisabled: boolean viewType: string } @@ -37,7 +36,6 @@ export default function Item( const className = classNames( 'uppy-ProviderBrowserItem', - // { 'uppy-ProviderBrowserItem--selected': isChecked }, { 'uppy-ProviderBrowserItem--disabled': isDisabled }, { 'uppy-ProviderBrowserItem--noPreview': itemIconString === 'video' }, ) diff --git a/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx b/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx index eb3d1fe7c1..1ad51b4c9f 100644 --- a/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx +++ b/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx @@ -7,7 +7,9 @@ import type { UnknownProviderPlugin, Uppy, PartialTree, - FileInPartialTree + PartialTreeFolder, + PartialTreeFolderNode, + PartialTreeFile, } from '@uppy/core/lib/Uppy.ts' import type { Body, Meta } from '@uppy/utils/lib/UppyFile' import type { CompanionFile } from '@uppy/utils/lib/CompanionFile.ts' @@ -23,9 +25,11 @@ import View, { type ViewOptions } from '../View.ts' // @ts-ignore We don't want TS to generate types for the package.json import packageJson from '../../package.json' -function formatBreadcrumbs(breadcrumbs: FileInPartialTree[]): string { - return breadcrumbs - .map((directory) => directory.data.name) +function formatBreadcrumbs(breadcrumbs: PartialTreeFolder[]): string { + const nonrootFoldes = breadcrumbs + .filter((folder) => folder.type === 'folder') as PartialTreeFolderNode[] + return nonrootFoldes + .map((folder) => folder.data.name) .join('/') } @@ -86,10 +90,6 @@ export default class ProviderView extends View< username: string | undefined - nextPagePath: string | undefined - - isRootFolderFetched: boolean = false; - constructor( plugin: UnknownProviderPlugin, opts: ProviderViewOptions, @@ -111,7 +111,14 @@ export default class ProviderView extends View< // Set default state for the plugin this.plugin.setPluginState({ authenticated: undefined, // we don't know yet - partialTree: [], + partialTree: [ + { + type: 'root', + id: this.plugin.rootFolderId, + cached: false, + nextPagePath: this.plugin.rootFolderId + } + ], currentFolderId: null, filterInput: '', isSearchVisible: false, @@ -189,9 +196,8 @@ export default class ProviderView extends View< console.log(`____________________________________________GETTING FOLDER "${folderId}"`); // Returning cached folder const { partialTree } = this.plugin.getPluginState() - const thisFolderIsCached = - partialTree.find((folder) => folder.id === folderId)?.cached || - (folderId === this.plugin.rootFolderId && this.isRootFolderFetched) + const clickedFolder = partialTree.find((folder) => folder.id === folderId) as PartialTreeFolder | undefined + const thisFolderIsCached = clickedFolder && clickedFolder.cached if (thisFolderIsCached) { console.log("Folder was cached____________________________________________"); this.plugin.setPluginState({ currentFolderId: folderId, filterInput: '' }) @@ -218,65 +224,56 @@ export default class ProviderView extends View< let newFolders = currentItems.filter((i) => i.isFolder === true) let newFiles = currentItems.filter((i) => i.isFolder === false) - if (!this.isRootFolderFetched) { - console.log("creating a new partial tree!"); + const clickedFolder : PartialTreeFolder = partialTree.find((folder) => folder.id === folderId)! as PartialTreeFolder - const newPartialTree : PartialTree = [ - ...newFolders.map((folder) => ({ - id: folder.requestPath, parentId: folderId, data: folder, - status: "unchecked", cached: false, nextPagePath: folder.requestPath - })) as FileInPartialTree[], - ...newFiles.map((file) => ({ - id: file.requestPath, parentId: folderId, - status: "unchecked", data: file - })) as FileInPartialTree[] - ] + // If selected folder is already filled in, don't refill it (because that would make it lose deep state!) + // Otherwise, cache the current folder! + if (clickedFolder && !clickedFolder.cached) { + const newlyAddedItemStatus = (clickedFolder.type === 'folder' && clickedFolder.status === 'checked') ? 'checked' : 'unchecked'; + const folders : PartialTreeFolderNode[] = newFolders.map((folder) => ({ + type: 'folder', + id: folder.requestPath, - console.log({ newPartialTree }); - - this.isRootFolderFetched = true - this.plugin.setPluginState({ partialTree: newPartialTree, currentFolderId: folderId, filterInput: '' }) - } else { - console.log("appending to existing partial tree!"); - const clickedFolder : FileInPartialTree = partialTree.find((folder) => folder.id === folderId)! - - // If selected folder is already filled in, don't refill it (because that would make it lose deep state!) - // Otherwise, cache the current folder! - if (clickedFolder && !clickedFolder.cached) { - const clickedFolderContents : FileInPartialTree[] = [ - ...newFolders.map((folder) => ({ - id: folder.requestPath, parentId: clickedFolder.id, data: folder, - status: clickedFolder.status, cached: false, - })), - ...newFiles.map((file) => ({ - id: file.requestPath, parentId: clickedFolder.id, data: file, - status: clickedFolder.status - })), - ] - - // just doing `clickedFolder.cached = true` in a non-mutating way - const updatedClickedFolder : FileInPartialTree = { ...clickedFolder, cached: true, nextPagePath: currentPagePath } - const partialTreeWithUpdatedClickedFolder = partialTree.map((folder) => - folder.id === updatedClickedFolder.id ? - updatedClickedFolder : - folder - ) - - const newPartialTree = [ - ...partialTreeWithUpdatedClickedFolder, - ...clickedFolderContents - ] - - console.log({ newPartialTree, folderId }); - - this.plugin.setPluginState({ - partialTree: newPartialTree, - currentFolderId: folderId, - filterInput: '' - }) + cached: false, + nextPagePath: folder.requestPath, + + status: newlyAddedItemStatus, + parentId: clickedFolder.id, + data: folder, + })) + const files : PartialTreeFile[] = newFiles.map((file) => ({ + type: 'file', + id: file.requestPath, + + status: newlyAddedItemStatus, + parentId: clickedFolder.id, + data: file, + })) + + // just doing `clickedFolder.cached = true` in a non-mutating way + const updatedClickedFolder : PartialTreeFolder = { + ...clickedFolder, + cached: true, + nextPagePath: currentPagePath } - } + const partialTreeWithUpdatedClickedFolder = partialTree.map((folder) => + folder.id === updatedClickedFolder.id ? + updatedClickedFolder : + folder + ) + + const newPartialTree = [ + ...partialTreeWithUpdatedClickedFolder, + ...folders, + ...files + ] + this.plugin.setPluginState({ + partialTree: newPartialTree, + currentFolderId: folderId, + filterInput: '' + }) + } }) @@ -305,8 +302,6 @@ export default class ProviderView extends View< } finally { this.setLoading(false) } - - this.lastCheckbox = undefined } /** @@ -383,7 +378,7 @@ export default class ProviderView extends View< async handleScroll(event: Event): Promise { const { partialTree, currentFolderId } = this.plugin.getPluginState() - const currentFolder = partialTree.find((i) => i.id === currentFolderId)! + const currentFolder = partialTree.find((i) => i.id === currentFolderId) as PartialTreeFolder if (this.shouldHandleScroll(event) && currentFolder.nextPagePath) { this.isHandlingScroll = true @@ -398,26 +393,36 @@ export default class ProviderView extends View< let newFolders = items.filter((i) => i.isFolder === true) let newFiles = items.filter((i) => i.isFolder === false) - // TODO nextPagePath shoud be inserted into .partialTree here - // just doing `clickedFolder.cached = true` in a non-mutating way - const updatedClickedFolder : FileInPartialTree = { ...currentFolder, nextPagePath } - const partialTreeWithUpdatedCurrentFolder = partialTree.map((folder) => - folder.id === updatedClickedFolder.id ? - updatedClickedFolder : - folder + const scrolledFolder : PartialTreeFolder = {...currentFolder, nextPagePath } + const partialTreeWithUpdatedScrolledFolder = partialTree.map((folder) => + folder.id === scrolledFolder.id ? scrolledFolder : folder ) + const newlyAddedItemStatus = (scrolledFolder.type === 'folder' && scrolledFolder.status === 'checked') ? 'checked' : 'unchecked'; + const folders : PartialTreeFolderNode[] = newFolders.map((folder) => ({ + type: 'folder', + id: folder.requestPath, + + cached: false, + nextPagePath: folder.requestPath, + + status: newlyAddedItemStatus, + parentId: scrolledFolder.id, + data: folder, + })) + const files : PartialTreeFile[] = newFiles.map((file) => ({ + type: 'file', + id: file.requestPath, + + status: newlyAddedItemStatus, + parentId: scrolledFolder.id, + data: file, + })) - const newPartialTree = [ - ...partialTreeWithUpdatedCurrentFolder, - ...newFolders.map((folder) => ({ - id: folder.requestPath, parentId: currentFolderId, data: folder, - status: "unchecked", cached: false, nextPagePath: folder.requestPath - })) as FileInPartialTree[], - ...newFiles.map((file) => ({ - id: file.requestPath, parentId: currentFolderId, data: file, - status: "unchecked" - })) as FileInPartialTree[] + const newPartialTree : PartialTree = [ + ...partialTreeWithUpdatedScrolledFolder, + ...folders, + ...files ] this.plugin.setPluginState({ partialTree: newPartialTree }) @@ -474,127 +479,129 @@ export default class ProviderView extends View< } async donePicking(): Promise { - this.setLoading(true) - try { - await this.#withAbort(async (signal) => { - const { partialTree } = this.plugin.getPluginState() - const currentSelection = partialTree.filter((item) => item.status === "checked") - - const messages: string[] = [] - const newFiles: CompanionFile[] = [] - - for (const selectedItem of currentSelection) { - const requestPath = selectedItem.id - - const withRelDirPath = (newItem: CompanionFile) => ({ - ...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 as string) - .replace(selectedItem.data.absDirPath as string, '') - .replace(/^\//, ''), - }) - - if (selectedItem.data.isFolder) { - let isEmpty = true - let numNewFiles = 0 - - const queue = new PQueue({ concurrency: 6 }) - - const onFiles = (files: CompanionFile[]) => { - 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 - } - } - - await this.#recursivelyListAllFiles({ - requestPath, - absDirPath: prependPath( - selectedItem.data.absDirPath, - selectedItem.data.name, - ), - relDirPath: selectedItem.data.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.data.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.data.name, - }) - } - - messages.push(message) - } else { - newFiles.push(withRelDirPath(selectedItem.data)) - } - } + // this.setLoading(true) + // try { + // await this.#withAbort(async (signal) => { + // const { partialTree } = this.plugin.getPluginState() + // const currentSelection = partialTree.filter((item) => item.status === "checked") + + // const messages: string[] = [] + // const newFiles: CompanionFile[] = [] + + // for (const selectedItem of currentSelection) { + // const requestPath = selectedItem.id + + // const withRelDirPath = (newItem: CompanionFile) => ({ + // ...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 as string) + // .replace(selectedItem.data.absDirPath as string, '') + // .replace(/^\//, ''), + // }) + + // if (selectedItem.data.isFolder) { + // let isEmpty = true + // let numNewFiles = 0 + + // const queue = new PQueue({ concurrency: 6 }) + + // const onFiles = (files: CompanionFile[]) => { + // 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 + // } + // } + + // await this.#recursivelyListAllFiles({ + // requestPath, + // absDirPath: prependPath( + // selectedItem.data.absDirPath, + // selectedItem.data.name, + // ), + // relDirPath: selectedItem.data.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.data.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.data.name, + // }) + // } + + // messages.push(message) + // } else { + // newFiles.push(withRelDirPath(selectedItem.data)) + // } + // } + + // // 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 files from a remote provider') + // this.plugin.uppy.addFiles( + // // @ts-expect-error `addFiles` expects `body` to be `File` or `Blob`, + // // but as the todo comment in `View.ts` indicates, we strangly pass `CompanionFile` as `body`. + // // For now it's better to ignore than to have a potential breaking change. + // newFiles.map((file) => this.getTagFile(file, this.requestClientId)), + // ) + + // this.plugin.setPluginState({ filterInput: '' }) + // messages.forEach((message) => this.plugin.uppy.info(message)) + + // this.clearSelection() + // }) + // } catch (err) { + // this.handleError(err) + // } finally { + // this.setLoading(false) + // } + } - // 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 files from a remote provider') - this.plugin.uppy.addFiles( - // @ts-expect-error `addFiles` expects `body` to be `File` or `Blob`, - // but as the todo comment in `View.ts` indicates, we strangly pass `CompanionFile` as `body`. - // For now it's better to ignore than to have a potential breaking change. - newFiles.map((file) => this.getTagFile(file, this.requestClientId)), - ) + getBreadcrumbs = () : PartialTreeFolder[] => { + const { partialTree, currentFolderId } = this.plugin.getPluginState() + if (!currentFolderId) return [] - this.plugin.setPluginState({ filterInput: '' }) - messages.forEach((message) => this.plugin.uppy.info(message)) + const breadcrumbs : PartialTreeFolder[] = [] + let parent = partialTree.find((folder) => folder.id === currentFolderId) as PartialTreeFolder + while (true) { + breadcrumbs.push(parent) + if (parent.type === 'root') break - this.clearSelection() - }) - } catch (err) { - this.handleError(err) - } finally { - this.setLoading(false) + parent = partialTree.find((folder) => folder.id === (parent as PartialTreeFolderNode).parentId) as PartialTreeFolder } - } - getBreadcrumbs = () => { - const { partialTree, currentFolderId } = this.plugin.getPluginState() - const breadcrumbs = [] - if (partialTree && currentFolderId) { - const currentFolder = partialTree.find((folder) => folder.id === currentFolderId) - let parent = currentFolder - while (parent) { - breadcrumbs.push(parent) - parent = partialTree.find((folder) => folder.id === parent!.parentId) - } - } return breadcrumbs.toReversed() } @@ -628,7 +635,7 @@ export default class ProviderView extends View< i18n, } - const displayedPartialTree = filterItems(partialTree.filter((item) => item.parentId === currentFolderId)) + const displayedPartialTree = filterItems(partialTree.filter((item) => item.type !== 'root' && item.parentId === currentFolderId)) as (PartialTreeFile | PartialTreeFolderNode)[] const browserProps = { toggleCheckbox: this.toggleCheckbox.bind(this), @@ -645,7 +652,7 @@ export default class ProviderView extends View< searchOnInput: true, searchInputLabel: i18n('filter'), clearSearchLabel: i18n('resetFilter'), - currentSelection: partialTree.filter((item) => item.status === "checked"), + currentSelection: partialTree.filter((item) => item.type !== 'root' && item.status === "checked") as (PartialTreeFile | PartialTreeFolderNode)[], noResultsLabel: i18n('noFilesFound'), logout: this.logout, diff --git a/packages/@uppy/provider-views/src/View.ts b/packages/@uppy/provider-views/src/View.ts index 7e09d01c9b..d590ea34f2 100644 --- a/packages/@uppy/provider-views/src/View.ts +++ b/packages/@uppy/provider-views/src/View.ts @@ -1,7 +1,8 @@ import type { - FileInPartialTree, PartialTree, - StatusInPartialTree, + PartialTreeFile, + PartialTreeFolder, + PartialTreeFolderNode, UnknownProviderPlugin, UnknownSearchProviderPlugin, } from '@uppy/core/lib/Uppy' @@ -177,14 +178,15 @@ export default class View< return tagFile } - filterItems = (items: FileInPartialTree[]): FileInPartialTree[] => { - const state = this.plugin.getPluginState() - if (!state.filterInput || state.filterInput === '') { + filterItems = (items: PartialTree): PartialTree => { + const { filterInput } = this.plugin.getPluginState() + if (!filterInput || filterInput === '') { return items } return items.filter((item) => { return ( - item.data?.name.toLowerCase().indexOf(state.filterInput.toLowerCase()) !== + item.type !== 'root' && + item.data.name.toLowerCase().indexOf(filterInput.toLowerCase()) !== -1 ) }) @@ -201,7 +203,7 @@ export default class View< * toggle multiple checkboxes at once, which is done by getting all files * in between last checked file and current one. */ - toggleCheckbox(e: Event, ourItem: FileInPartialTree) { + toggleCheckbox(e: Event, ourItem: PartialTreeFolderNode | PartialTreeFile) { e.stopPropagation() e.preventDefault() @@ -210,20 +212,19 @@ export default class View< // if newStatus is "checked" - percolate down "checked" // if newStatus is "unchecked" - percolate down "unchecked" - const percolateDown = (currentFile: FileInPartialTree, status: StatusInPartialTree) => { - const children : FileInPartialTree[] = newPartialTree.filter((item) => item.parentId === currentFile.id) as FileInPartialTree[] + const percolateDown = (clickedItem: PartialTreeFolderNode | PartialTreeFile, status: 'checked' | 'unchecked') => { + const children = newPartialTree.filter((item) => item.type !== 'root' && item.parentId === clickedItem.id) as (PartialTreeFolderNode | PartialTreeFile)[] children.forEach((item) => { item.status = status percolateDown(item, status) }) } // we do something to all of its parents. - const percolateUp = (currentFile: FileInPartialTree) => { - const parentFolder = newPartialTree.find((item) => item.id === currentFile.parentId) + const percolateUp = (currentItem: PartialTreeFolderNode | PartialTreeFile) => { + const parentFolder = newPartialTree.find((item) => item.id === currentItem.parentId)! as PartialTreeFolder + if (parentFolder.type === 'root') return - if (!parentFolder) return - - const parentsChildren = newPartialTree.filter((item) => item.parentId === parentFolder.id) as FileInPartialTree[] + const parentsChildren = newPartialTree.filter((item) => item.type !== 'root' && item.parentId === parentFolder.id) as (PartialTreeFile | PartialTreeFolderNode)[] const areAllChildrenChecked = parentsChildren.every((item) => item.status === "checked") const areAllChildrenUnchecked = parentsChildren.every((item) => item.status === "unchecked") @@ -240,7 +241,7 @@ export default class View< // Shift-clicking selects a single consecutive list of items // starting at the previous click. - const inThisFolder = this.filterItems(newPartialTree.filter((item) => item.parentId === currentFolderId)) + const inThisFolder = this.filterItems(newPartialTree.filter((item) => item.type !== 'root' && item.parentId === currentFolderId)) as (PartialTreeFile | PartialTreeFolderNode)[] const prevIndex = inThisFolder.findIndex((item) => item.id === this.lastCheckbox) if (prevIndex !== -1 && this.isShiftKeyPressed) { const newIndex = inThisFolder.findIndex((item) => item.id === ourItem.id) @@ -250,23 +251,23 @@ export default class View< ).map((item) => item.id) const newlyCheckedItems = newPartialTree - .filter((item) => toMarkAsChecked.includes(item.id)) + .filter((item) => item.type !== 'root' && toMarkAsChecked.includes(item.id)) as (PartialTreeFile | PartialTreeFolderNode)[] - newlyCheckedItems.forEach((item) => item.status = "checked") + newlyCheckedItems.forEach((item) => item.status = 'checked') newlyCheckedItems.forEach((item) => { - percolateDown(item, "checked") + percolateDown(item, 'checked') }) percolateUp(ourItem) } else { - const ourItemInNewTree = newPartialTree.find((item) => item.id === ourItem.id)! - ourItemInNewTree.status = ourItem.status === "checked" ? "unchecked" : "checked" + const ourItemInNewTree = newPartialTree.find((item) => item.id === ourItem.id) as (PartialTreeFile | PartialTreeFolderNode) + ourItemInNewTree.status = ourItem.status === 'checked' ? 'unchecked' : 'checked' percolateDown(ourItem, ourItemInNewTree.status) percolateUp(ourItem) } this.plugin.setPluginState({ partialTree: newPartialTree }) - this.lastCheckbox = ourItem.id + this.lastCheckbox = ourItem.id! } setLoading(loading: boolean | string): void { From 5d6e92c9bab9bd6423d5a985b292d9bab27f27b9 Mon Sep 17 00:00:00 2001 From: Evgenia Karunus Date: Thu, 4 Apr 2024 05:11:37 +0400 Subject: [PATCH 027/170] types - reintroduce `StatusInPartialTree` --- packages/@uppy/core/src/Uppy.ts | 11 +++++++---- .../provider-views/src/Item/components/GridLi.tsx | 4 ++-- .../provider-views/src/Item/components/ListLi.tsx | 4 ++-- packages/@uppy/provider-views/src/Item/index.tsx | 3 ++- 4 files changed, 13 insertions(+), 9 deletions(-) diff --git a/packages/@uppy/core/src/Uppy.ts b/packages/@uppy/core/src/Uppy.ts index 0baa54194a..f92e3b2cd4 100644 --- a/packages/@uppy/core/src/Uppy.ts +++ b/packages/@uppy/core/src/Uppy.ts @@ -64,17 +64,18 @@ export type UnknownPlugin< // ids are always `string`s, except the root folder's id can be `null` export type PartialTreeId = string | null +export type PartialTreeStatusFile = 'checked' | 'unchecked' +export type PartialTreeStatus = PartialTreeStatusFile | 'partial' + export type PartialTreeFile = { type: 'file' id: string - status: 'checked' | 'unchecked' + status: PartialTreeStatusFile parentId: PartialTreeId data: CompanionFile } -export type PartialTreeFolder = PartialTreeFolderNode | PartialTreeFolderRoot - export type PartialTreeFolderNode = { type: 'folder' id: string @@ -82,7 +83,7 @@ export type PartialTreeFolderNode = { cached: boolean nextPagePath: PartialTreeId - status: 'checked' | 'unchecked' | 'partial' + status: PartialTreeStatus parentId: PartialTreeId data: CompanionFile } @@ -95,6 +96,8 @@ export type PartialTreeFolderRoot = { nextPagePath: PartialTreeId } +export type PartialTreeFolder = PartialTreeFolderNode | PartialTreeFolderRoot + export type PartialTree = (PartialTreeFile | PartialTreeFolder)[] export type UnknownProviderPluginState = { diff --git a/packages/@uppy/provider-views/src/Item/components/GridLi.tsx b/packages/@uppy/provider-views/src/Item/components/GridLi.tsx index 41d79d15c7..72740a46f6 100644 --- a/packages/@uppy/provider-views/src/Item/components/GridLi.tsx +++ b/packages/@uppy/provider-views/src/Item/components/GridLi.tsx @@ -3,13 +3,13 @@ import { h } from 'preact' import classNames from 'classnames' import type { RestrictionError } from '@uppy/core/lib/Restricter' import type { Body, Meta } from '@uppy/utils/lib/UppyFile' -import type { StatusInPartialTree } from '@uppy/core/lib/Uppy' +import type { PartialTreeStatus } from '@uppy/core/lib/Uppy' type GridListItemProps = { className: string isDisabled: boolean restrictionError?: RestrictionError | null - status: StatusInPartialTree + status: PartialTreeStatus title?: string itemIconEl: any showTitles?: boolean diff --git a/packages/@uppy/provider-views/src/Item/components/ListLi.tsx b/packages/@uppy/provider-views/src/Item/components/ListLi.tsx index 8c872bde74..1a5984b0a5 100644 --- a/packages/@uppy/provider-views/src/Item/components/ListLi.tsx +++ b/packages/@uppy/provider-views/src/Item/components/ListLi.tsx @@ -1,6 +1,6 @@ /* eslint-disable react/require-default-props */ import type { RestrictionError } from '@uppy/core/lib/Restricter' -import type { StatusInPartialTree } from '@uppy/core/lib/Uppy' +import type { PartialTreeStatus } from '@uppy/core/lib/Uppy' import type { Body, Meta } from '@uppy/utils/lib/UppyFile' import { h } from 'preact' @@ -16,7 +16,7 @@ type ListItemProps = { isDisabled: boolean restrictionError?: RestrictionError | null isCheckboxDisabled: boolean - status: StatusInPartialTree + status: PartialTreeStatus toggleCheckbox: (event: Event) => void recordShiftKeyPress: (event: KeyboardEvent | MouseEvent) => void type: string diff --git a/packages/@uppy/provider-views/src/Item/index.tsx b/packages/@uppy/provider-views/src/Item/index.tsx index e9fc4b028d..7433d4bb25 100644 --- a/packages/@uppy/provider-views/src/Item/index.tsx +++ b/packages/@uppy/provider-views/src/Item/index.tsx @@ -9,6 +9,7 @@ import type { Meta, Body } from '@uppy/utils/lib/UppyFile' import ItemIcon from './components/ItemIcon.tsx' import GridListItem from './components/GridLi.tsx' import ListItem from './components/ListLi.tsx' +import type { PartialTreeStatus } from '@uppy/core/lib/Uppy.ts' type ItemProps = { showTitles: boolean @@ -23,7 +24,7 @@ type ItemProps = { type: 'folder' | 'file' author?: CompanionFile['author'] getItemIcon: () => string - status: 'checked' | 'unchecked' | 'partial' + status: PartialTreeStatus isDisabled: boolean viewType: string } From f0b7f8123be86db409dd68813f919d4735d94332 Mon Sep 17 00:00:00 2001 From: Evgenia Karunus Date: Thu, 4 Apr 2024 05:37:06 +0400 Subject: [PATCH 028/170] - made Unsplash work with the new structure --- packages/@uppy/core/src/Uppy.ts | 1 - .../SearchProviderView/SearchProviderView.tsx | 41 ++++++++++--------- packages/@uppy/unsplash/src/Unsplash.tsx | 5 --- 3 files changed, 22 insertions(+), 25 deletions(-) diff --git a/packages/@uppy/core/src/Uppy.ts b/packages/@uppy/core/src/Uppy.ts index f92e3b2cd4..f35aac79ec 100644 --- a/packages/@uppy/core/src/Uppy.ts +++ b/packages/@uppy/core/src/Uppy.ts @@ -158,7 +158,6 @@ export type UnknownSearchProviderPlugin< M extends Meta, B extends Body, > = UnknownPlugin & { - onFirstRender: () => void title: string icon: () => JSX.Element provider: CompanionClientSearchProvider diff --git a/packages/@uppy/provider-views/src/SearchProviderView/SearchProviderView.tsx b/packages/@uppy/provider-views/src/SearchProviderView/SearchProviderView.tsx index 7963eee11d..b685ff8827 100644 --- a/packages/@uppy/provider-views/src/SearchProviderView/SearchProviderView.tsx +++ b/packages/@uppy/provider-views/src/SearchProviderView/SearchProviderView.tsx @@ -1,7 +1,7 @@ import { h } from 'preact' import type { Body, Meta } from '@uppy/utils/lib/UppyFile' -import type { PartialTree, StatusInPartialTree, UnknownSearchProviderPlugin } from '@uppy/core/lib/Uppy.ts' +import type { PartialTree, PartialTreeFile, PartialTreeFolderNode, PartialTreeStatusFile, UnknownSearchProviderPlugin, UnknownSearchProviderPluginState } from '@uppy/core/lib/Uppy.ts' import type { DefinePluginOpts } from '@uppy/core/lib/BasePlugin.ts' import type Uppy from '@uppy/core' import type { CompanionFile } from '@uppy/utils/lib/CompanionFile' @@ -14,12 +14,18 @@ import View, { type ViewOptions } from '../View.ts' // @ts-ignore We don't want TS to generate types for the package.json import packageJson from '../../package.json' -const defaultState = { +const defaultState : Partial = { isInputMode: true, - breadcrumbs: [], filterInput: '', searchTerm: null, - partialTree: [], + partialTree: [ + { + type: 'root', + id: null, + cached: false, + nextPagePath: null + } + ], currentFolderId: null, } @@ -84,18 +90,18 @@ export default class SearchProviderView< this.plugin.setPluginState(defaultState) } - #updateFilesAndInputMode(res: Res, files: PartialTree): void { + #updateFilesAndInputMode(res: Res): void { this.nextPageQuery = res.nextPageQuery const { partialTree } = this.plugin.getPluginState() const newPartialTree : PartialTree = [ ...partialTree, ...res.items.map((item) => ({ + type: 'file', id: item.requestPath, - cached: null, - status: "unchecked" as StatusInPartialTree, + status: 'unchecked', parentId: null, data: item - })) + }) as PartialTreeFile) ] this.plugin.setPluginState({ partialTree: newPartialTree, @@ -114,7 +120,7 @@ export default class SearchProviderView< this.setLoading(true) try { const res = await this.provider.search(query) - this.#updateFilesAndInputMode(res, []) + this.#updateFilesAndInputMode(res) } catch (err) { this.handleError(err) } finally { @@ -137,10 +143,10 @@ export default class SearchProviderView< this.isHandlingScroll = true try { - const { partialTree, searchTerm } = this.plugin.getPluginState() + const { searchTerm } = this.plugin.getPluginState() const response = await this.provider.search(searchTerm!, query) - this.#updateFilesAndInputMode(response, partialTree) + this.#updateFilesAndInputMode(response) } catch (error) { this.handleError(error) } finally { @@ -152,8 +158,9 @@ export default class SearchProviderView< donePicking(): void { const { partialTree } = this.plugin.getPluginState() this.plugin.uppy.log('Adding remote search provider files') + const files = partialTree.filter((i) => i.type !== 'root' && i.status === 'checked') as PartialTreeFile[] this.plugin.uppy.addFiles( - partialTree.map((file) => this.getTagFile(file.data)), + files.map((file) => this.getTagFile(file.data)), ) this.resetPluginState() } @@ -162,25 +169,21 @@ export default class SearchProviderView< state: unknown, viewOptions: Omit, 'provider'> = {}, ): JSX.Element { - const { didFirstRender, isInputMode, searchTerm } = + const { isInputMode, searchTerm } = this.plugin.getPluginState() const { i18n } = this.plugin.uppy - if (!didFirstRender) { - this.preFirstRender() - } - const targetViewOptions = { ...this.opts, ...viewOptions } const { loading, partialTree, currentFolderId } = this.plugin.getPluginState() const { filterItems, recordShiftKeyPress } = this - const displayedPartialTree = filterItems(partialTree.filter((item) => item.parentId === currentFolderId)) + const displayedPartialTree = filterItems(partialTree.filter((item) => item.type !== 'root' && item.parentId === currentFolderId)) as PartialTreeFile[] const browserProps = { toggleCheckbox: this.toggleCheckbox.bind(this), recordShiftKeyPress, - currentSelection: partialTree.filter((item) => item.status === "checked"), + currentSelection: partialTree.filter((i) => i.type !== 'root' && i.status === 'checked') as PartialTreeFile[], displayedPartialTree, handleScroll: this.handleScroll, done: this.donePicking, diff --git a/packages/@uppy/unsplash/src/Unsplash.tsx b/packages/@uppy/unsplash/src/Unsplash.tsx index a5c8d5403b..b1ff41dec6 100644 --- a/packages/@uppy/unsplash/src/Unsplash.tsx +++ b/packages/@uppy/unsplash/src/Unsplash.tsx @@ -93,11 +93,6 @@ export default class Unsplash extends UIPlugin< } } - // eslint-disable-next-line class-methods-use-this - async onFirstRender(): Promise { - // do nothing - } - render(state: unknown): ComponentChild { return this.view.render(state) } From 992a913781aabfc965bf133b8780bdfdc15dc0d0 Mon Sep 17 00:00:00 2001 From: Evgenia Karunus Date: Fri, 5 Apr 2024 07:29:52 +0400 Subject: [PATCH 029/170] - preemptive cleaning of `.absDirPath` and `.relDirPath` --- .../src/ProviderView/ProviderView.tsx | 24 ++++--------------- 1 file changed, 4 insertions(+), 20 deletions(-) diff --git a/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx b/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx index 1ad51b4c9f..3ccaca0d55 100644 --- a/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx +++ b/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx @@ -161,15 +161,7 @@ export default class ProviderView extends View< } } - async #list({ - requestPath, - absDirPath, - signal, - }: { - requestPath: string | null - absDirPath: string - signal: AbortSignal - }) { + async #list({ requestPath, signal }: { requestPath: string | null, signal: AbortSignal }) { const { username, nextPagePath, items } = await this.provider.list<{ username: string nextPagePath: string @@ -177,13 +169,7 @@ export default class ProviderView extends View< }>(requestPath || undefined, { signal }) this.username = username || this.username - return { - items: items.map((item) => ({ - ...item, - absDirPath, - })), - nextPagePath, - } + return { items, nextPagePath } } /** @@ -213,7 +199,6 @@ export default class ProviderView extends View< do { const { items, nextPagePath } = await this.#list({ requestPath: currentPagePath, - absDirPath: formatBreadcrumbs(this.getBreadcrumbs()), signal }) currentPagePath = nextPagePath @@ -386,8 +371,7 @@ export default class ProviderView extends View< await this.#withAbort(async (signal) => { const { items, nextPagePath } = await this.#list({ - requestPath: currentFolder.nextPagePath!, - absDirPath: formatBreadcrumbs(this.getBreadcrumbs()), + requestPath: currentFolder.nextPagePath, signal }) let newFolders = items.filter((i) => i.isFolder === true) @@ -453,7 +437,7 @@ export default class ProviderView extends View< let curPath = requestPath while (curPath) { - const res = await this.#list({ requestPath: curPath, absDirPath, signal }) + const res = await this.#list({ requestPath: curPath, signal }) curPath = res.nextPagePath const files = res.items.filter((item) => !item.isFolder) From e2ab1138bec3539ddebbf90b6ff615294e937d28 Mon Sep 17 00:00:00 2001 From: Evgenia Karunus Date: Fri, 5 Apr 2024 10:14:44 +0400 Subject: [PATCH 030/170] - give `.nextPagePath` a rigorous type --- packages/@uppy/core/src/Uppy.ts | 9 +++++++-- .../src/ProviderView/ProviderView.tsx | 16 ++++++---------- .../@uppy/utils/src/CompanionClientProvider.ts | 12 +++++++++--- 3 files changed, 22 insertions(+), 15 deletions(-) diff --git a/packages/@uppy/core/src/Uppy.ts b/packages/@uppy/core/src/Uppy.ts index f35aac79ec..a90bf6d99d 100644 --- a/packages/@uppy/core/src/Uppy.ts +++ b/packages/@uppy/core/src/Uppy.ts @@ -81,7 +81,12 @@ export type PartialTreeFolderNode = { id: string cached: boolean - nextPagePath: PartialTreeId + // .nextPagePath can be: + // `undefined` - when we don't yet know the next page's url. + // `null` - *strictly* when we fetched all pages already. + // string - *strictly* when there are still pages to fetch. + // Notice we can't get away with just `null` and string, because some root folders have id: `null`, meaning we'd confuse "we're done with all pages in this folder" state with "we didn't yet fetch this folder at all" state. + nextPagePath: PartialTreeId | undefined status: PartialTreeStatus parentId: PartialTreeId @@ -93,7 +98,7 @@ export type PartialTreeFolderRoot = { id: PartialTreeId cached: boolean - nextPagePath: PartialTreeId + nextPagePath: PartialTreeId | undefined } export type PartialTreeFolder = PartialTreeFolderNode | PartialTreeFolderRoot diff --git a/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx b/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx index 3ccaca0d55..0fbc37e23e 100644 --- a/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx +++ b/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx @@ -116,7 +116,7 @@ export default class ProviderView extends View< type: 'root', id: this.plugin.rootFolderId, cached: false, - nextPagePath: this.plugin.rootFolderId + nextPagePath: undefined } ], currentFolderId: null, @@ -162,11 +162,7 @@ export default class ProviderView extends View< } async #list({ requestPath, signal }: { requestPath: string | null, signal: AbortSignal }) { - const { username, nextPagePath, items } = await this.provider.list<{ - username: string - nextPagePath: string - items: CompanionFile[] - }>(requestPath || undefined, { signal }) + const { username, nextPagePath, items } = await this.provider.list(requestPath, { signal }) this.username = username || this.username return { items, nextPagePath } @@ -220,7 +216,7 @@ export default class ProviderView extends View< id: folder.requestPath, cached: false, - nextPagePath: folder.requestPath, + nextPagePath: undefined, status: newlyAddedItemStatus, parentId: clickedFolder.id, @@ -371,13 +367,13 @@ export default class ProviderView extends View< await this.#withAbort(async (signal) => { const { items, nextPagePath } = await this.#list({ - requestPath: currentFolder.nextPagePath, + requestPath: currentFolder.nextPagePath!, signal }) let newFolders = items.filter((i) => i.isFolder === true) let newFiles = items.filter((i) => i.isFolder === false) - // just doing `clickedFolder.cached = true` in a non-mutating way + // just doing `scrolledFolder.nextPagePath = ...` in a non-mutating way const scrolledFolder : PartialTreeFolder = {...currentFolder, nextPagePath } const partialTreeWithUpdatedScrolledFolder = partialTree.map((folder) => folder.id === scrolledFolder.id ? scrolledFolder : folder @@ -388,7 +384,7 @@ export default class ProviderView extends View< id: folder.requestPath, cached: false, - nextPagePath: folder.requestPath, + nextPagePath: undefined, status: newlyAddedItemStatus, parentId: scrolledFolder.id, diff --git a/packages/@uppy/utils/src/CompanionClientProvider.ts b/packages/@uppy/utils/src/CompanionClientProvider.ts index e4bd94c8b3..741c08e313 100644 --- a/packages/@uppy/utils/src/CompanionClientProvider.ts +++ b/packages/@uppy/utils/src/CompanionClientProvider.ts @@ -1,3 +1,5 @@ +import type { CompanionFile } from "./CompanionFile" + export type RequestOptions = { method?: string data?: Record @@ -24,10 +26,14 @@ export interface CompanionClientProvider { login(options?: RequestOptions): Promise logout(options?: RequestOptions): Promise fetchPreAuthToken(): Promise - list( - directory: string | undefined, + list( + directory: string | null, options: RequestOptions, - ): Promise + ): Promise<{ + username: string + nextPagePath: string | null + items: CompanionFile[] + }> } export interface CompanionClientSearchProvider { name: string From 21b0677302166a66a924e1e3398ad1648957e945 Mon Sep 17 00:00:00 2001 From: Evgenia Karunus Date: Fri, 5 Apr 2024 10:21:21 +0400 Subject: [PATCH 031/170] - make `.nextPagePath` & `.cached` a composite key --- packages/@uppy/core/src/Uppy.ts | 14 ++++++++------ .../src/ProviderView/ProviderView.tsx | 6 +++--- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/packages/@uppy/core/src/Uppy.ts b/packages/@uppy/core/src/Uppy.ts index a90bf6d99d..75230ba3a8 100644 --- a/packages/@uppy/core/src/Uppy.ts +++ b/packages/@uppy/core/src/Uppy.ts @@ -82,11 +82,13 @@ export type PartialTreeFolderNode = { cached: boolean // .nextPagePath can be: - // `undefined` - when we don't yet know the next page's url. - // `null` - *strictly* when we fetched all pages already. - // string - *strictly* when there are still pages to fetch. - // Notice we can't get away with just `null` and string, because some root folders have id: `null`, meaning we'd confuse "we're done with all pages in this folder" state with "we didn't yet fetch this folder at all" state. - nextPagePath: PartialTreeId | undefined + // `null` - *strictly* + // when { cached: true } and we fetched all pages already + // OR + // when { cached: false } and our .nextPagePath is simply .id + // string - *strictly* when { cached: true } and there are still pages to fetch. + // So, consider .cached and .nextPagePath a composite key of sorts, their combination create a specific meaning. + nextPagePath: PartialTreeId status: PartialTreeStatus parentId: PartialTreeId @@ -98,7 +100,7 @@ export type PartialTreeFolderRoot = { id: PartialTreeId cached: boolean - nextPagePath: PartialTreeId | undefined + nextPagePath: PartialTreeId } export type PartialTreeFolder = PartialTreeFolderNode | PartialTreeFolderRoot diff --git a/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx b/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx index 0fbc37e23e..a3123c8ac4 100644 --- a/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx +++ b/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx @@ -116,7 +116,7 @@ export default class ProviderView extends View< type: 'root', id: this.plugin.rootFolderId, cached: false, - nextPagePath: undefined + nextPagePath: null } ], currentFolderId: null, @@ -216,7 +216,7 @@ export default class ProviderView extends View< id: folder.requestPath, cached: false, - nextPagePath: undefined, + nextPagePath: null, status: newlyAddedItemStatus, parentId: clickedFolder.id, @@ -384,7 +384,7 @@ export default class ProviderView extends View< id: folder.requestPath, cached: false, - nextPagePath: undefined, + nextPagePath: null, status: newlyAddedItemStatus, parentId: scrolledFolder.id, From 266fb1ff8f5edec5b2ae2253f1e7a74777867234 Mon Sep 17 00:00:00 2001 From: Evgenia Karunus Date: Mon, 8 Apr 2024 12:50:35 +0400 Subject: [PATCH 032/170] - remove unnecessary indirection level --- packages/@uppy/provider-views/src/Browser.tsx | 87 ++------------- .../@uppy/provider-views/src/Item/index.tsx | 100 ++++++++++-------- 2 files changed, 60 insertions(+), 127 deletions(-) diff --git a/packages/@uppy/provider-views/src/Browser.tsx b/packages/@uppy/provider-views/src/Browser.tsx index 125b374cb0..c67a9dfd45 100644 --- a/packages/@uppy/provider-views/src/Browser.tsx +++ b/packages/@uppy/provider-views/src/Browser.tsx @@ -2,7 +2,6 @@ import { h } from 'preact' import classNames from 'classnames' -import remoteFileObjToLocal from '@uppy/utils/lib/remoteFileObjToLocal' // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore untyped import VirtualList from '@uppy/utils/lib/VirtualList' @@ -12,79 +11,7 @@ import type Uppy from '@uppy/core' import SearchFilterInput from './SearchFilterInput.tsx' import FooterActions from './FooterActions.tsx' import Item from './Item/index.tsx' -import type { PartialTree, PartialTreeFile, PartialTreeFolderNode, PartialTreeId } from '@uppy/core/lib/Uppy.ts' - -const VIRTUAL_SHARED_DIR = 'shared-with-me' - -type ListItemProps = { - currentSelection: any[] - uppyFiles: UppyFile[] - viewType: string - toggleCheckbox: (event: Event, file: (PartialTreeFile | PartialTreeFolderNode)) => void - recordShiftKeyPress: (event: KeyboardEvent | MouseEvent) => void - showTitles: boolean - i18n: I18n - validateRestrictions: Uppy['validateRestrictions'] - getFolder: (folderId: PartialTreeId) => void - f: (PartialTreeFile | PartialTreeFolderNode) -} - -function ListItem( - props: ListItemProps, -): JSX.Element { - const { - currentSelection, - uppyFiles, - viewType, - toggleCheckbox, - recordShiftKeyPress, - showTitles, - i18n, - validateRestrictions, - getFolder, - f, - } = props - - if (f.data.isFolder) { - return Item({ - showTitles, - viewType, - i18n, - id: f.id, - title: f.data.name, - getItemIcon: () => f.data.icon, - status: f.status, - toggleCheckbox: (event: Event) => toggleCheckbox(event, f), - recordShiftKeyPress, - type: 'folder', - // TODO: when was this supposed to be true? - isDisabled: false, - isCheckboxDisabled: f.id === VIRTUAL_SHARED_DIR, - handleFolderClick: () => getFolder(f.id), - }) - } - const restrictionError = validateRestrictions(remoteFileObjToLocal(f.data), [ - ...uppyFiles, - ...currentSelection, - ]) - - return Item({ - id: f.id, - title: f.data.name, - author: f.data.author, - getItemIcon: () => f.data.icon, - toggleCheckbox: (event: Event) => toggleCheckbox(event, f), - isCheckboxDisabled: false, - status: f.status, - recordShiftKeyPress, - showTitles, - viewType, - i18n, - type: 'file', - isDisabled: Boolean(restrictionError) && (f.status !== "checked"), - restrictionError, - }) -} +import type { PartialTreeFile, PartialTreeFolderNode } from '@uppy/core/lib/Uppy.ts' type BrowserProps = { displayedPartialTree: (PartialTreeFile | PartialTreeFolderNode)[], @@ -200,8 +127,8 @@ function Browser(
            ( - ( + ( i18n={i18n} validateRestrictions={validateRestrictions} getFolder={getFolder} - f={f} + file={file} /> )} rowHeight={31} @@ -230,8 +157,8 @@ function Browser( // making
              not focusable for firefox tabIndex={-1} > - {displayedPartialTree.map((f) => ( - ( + ( i18n={i18n} validateRestrictions={validateRestrictions} getFolder={getFolder} - f={f} + file={file} /> ))}
            diff --git a/packages/@uppy/provider-views/src/Item/index.tsx b/packages/@uppy/provider-views/src/Item/index.tsx index 7433d4bb25..ca650a557b 100644 --- a/packages/@uppy/provider-views/src/Item/index.tsx +++ b/packages/@uppy/provider-views/src/Item/index.tsx @@ -3,81 +3,87 @@ import { h } from 'preact' import classNames from 'classnames' import type { I18n } from '@uppy/utils/lib/Translator' -import type { CompanionFile } from '@uppy/utils/lib/CompanionFile' -import type { RestrictionError } from '@uppy/core/lib/Restricter.ts' -import type { Meta, Body } from '@uppy/utils/lib/UppyFile' +import type { Meta, Body, UppyFile } from '@uppy/utils/lib/UppyFile' import ItemIcon from './components/ItemIcon.tsx' import GridListItem from './components/GridLi.tsx' import ListItem from './components/ListLi.tsx' -import type { PartialTreeStatus } from '@uppy/core/lib/Uppy.ts' +import type { PartialTreeFile, PartialTreeFolderNode, PartialTreeId, Uppy } from '@uppy/core/lib/Uppy.ts' +import remoteFileObjToLocal from '@uppy/utils/lib/remoteFileObjToLocal' + +const VIRTUAL_SHARED_DIR = 'shared-with-me' type ItemProps = { + currentSelection: any[] + uppyFiles: UppyFile[] + viewType: string + toggleCheckbox: (event: Event, file: (PartialTreeFile | PartialTreeFolderNode)) => void + recordShiftKeyPress: (event: KeyboardEvent | MouseEvent) => void showTitles: boolean i18n: I18n - id: string - title: string - toggleCheckbox: (event: Event) => void - recordShiftKeyPress: (event: KeyboardEvent | MouseEvent) => void - handleFolderClick?: () => void - restrictionError?: RestrictionError | null - isCheckboxDisabled: boolean - type: 'folder' | 'file' - author?: CompanionFile['author'] - getItemIcon: () => string - status: PartialTreeStatus - isDisabled: boolean - viewType: string + validateRestrictions: Uppy['validateRestrictions'] + getFolder: (folderId: PartialTreeId) => void + file: PartialTreeFile | PartialTreeFolderNode } export default function Item( props: ItemProps, ): h.JSX.Element { - const { author, getItemIcon, isDisabled, viewType } = props - const itemIconString = getItemIcon() + const { currentSelection, uppyFiles, viewType, toggleCheckbox, recordShiftKeyPress, showTitles, i18n, validateRestrictions, getFolder, file } = props - const className = classNames( - 'uppy-ProviderBrowserItem', - { 'uppy-ProviderBrowserItem--disabled': isDisabled }, - { 'uppy-ProviderBrowserItem--noPreview': itemIconString === 'video' }, - ) + const restrictionError = file.data.isFolder ? null : validateRestrictions(remoteFileObjToLocal(file.data), [...uppyFiles, ...currentSelection]) + const isDisabled = file.data.isFolder ? false : (Boolean(restrictionError) && (file.status !== "checked")) - const itemIconEl = + const sharedProps = { + id: file.id, + title: file.data.name, + status: file.status, + + i18n, + toggleCheckbox: (event: Event) => toggleCheckbox(event, file), + viewType, + showTitles, + recordShiftKeyPress, + className: classNames( + 'uppy-ProviderBrowserItem', + { 'uppy-ProviderBrowserItem--disabled': isDisabled }, + { 'uppy-ProviderBrowserItem--noPreview': file.data.icon === 'video' }, + ), + itemIconEl: , + isDisabled, + } + + let ourProps = file.data.isFolder ? + { + ...sharedProps, + type: 'folder', + isCheckboxDisabled: file.id === VIRTUAL_SHARED_DIR, + handleFolderClick: () => getFolder(file.id), + } : + { + ...sharedProps, + isCheckboxDisabled: false, + type: 'file', + restrictionError, + } switch (viewType) { case 'grid': - return ( - - // eslint-disable-next-line react/jsx-props-no-spreading - {...props} - className={className} - itemIconEl={itemIconEl} - /> - ) + return {...ourProps} /> case 'list': return ( - - // eslint-disable-next-line react/jsx-props-no-spreading - {...props} - className={className} - itemIconEl={itemIconEl} - /> + {...ourProps} /> ) case 'unsplash': return ( - - // eslint-disable-next-line react/jsx-props-no-spreading - {...props} - className={className} - itemIconEl={itemIconEl} - > + {...ourProps} > - {author!.name} + {file.data.author!.name} ) From 9feeb86a3d3a0fce6638a08a8a7b8ffa95fcd419 Mon Sep 17 00:00:00 2001 From: Evgenia Karunus Date: Mon, 8 Apr 2024 13:04:39 +0400 Subject: [PATCH 033/170] css - factor out `.statusClassName` --- .../src/Item/components/GridLi.tsx | 18 +----------------- .../src/Item/components/ListLi.tsx | 12 +----------- .../@uppy/provider-views/src/Item/index.tsx | 2 ++ .../uppy-ProviderBrowser-viewType--grid.scss | 2 +- .../uppy-ProviderBrowser-viewType--list.scss | 14 ++++++++------ .../uppy-ProviderBrowserItem-checkbox.scss | 4 ++-- 6 files changed, 15 insertions(+), 37 deletions(-) diff --git a/packages/@uppy/provider-views/src/Item/components/GridLi.tsx b/packages/@uppy/provider-views/src/Item/components/GridLi.tsx index 72740a46f6..912abbcaec 100644 --- a/packages/@uppy/provider-views/src/Item/components/GridLi.tsx +++ b/packages/@uppy/provider-views/src/Item/components/GridLi.tsx @@ -36,22 +36,6 @@ function GridListItem( children, } = props - let statusClassName - if (status === "checked") { - statusClassName = "uppy-ProviderBrowserItem-checkbox--is-checked" - } else if (status === "unchecked") { - statusClassName = "" - } else if (status === "partial") { - statusClassName = "uppy-ProviderBrowserItem-checkbox--is-partial" - } - - const checkBoxClassName = classNames( - 'uppy-u-reset', - 'uppy-ProviderBrowserItem-checkbox', - 'uppy-ProviderBrowserItem-checkbox--grid', - statusClassName - ) - return (
          • ( > ( i18n, } = props - - let statusClassName - if (status === "checked") { - statusClassName = "uppy-ProviderBrowserItem-checkbox--is-checked" - } else if (status === "unchecked") { - statusClassName = "" - } else if (status === "partial") { - statusClassName = "uppy-ProviderBrowserItem-checkbox--is-partial" - } - return (
          • ( {!isCheckboxDisabled ? ( 'uppy-ProviderBrowserItem', { 'uppy-ProviderBrowserItem--disabled': isDisabled }, { 'uppy-ProviderBrowserItem--noPreview': file.data.icon === 'video' }, + { 'uppy-ProviderBrowserItem--is-checked': file.status === 'checked' }, + { 'uppy-ProviderBrowserItem--is-partial': file.status === 'partial' } ), itemIconEl: , isDisabled, diff --git a/packages/@uppy/provider-views/src/style/uppy-ProviderBrowser-viewType--grid.scss b/packages/@uppy/provider-views/src/style/uppy-ProviderBrowser-viewType--grid.scss index 761ab3d018..407575b79f 100644 --- a/packages/@uppy/provider-views/src/style/uppy-ProviderBrowser-viewType--grid.scss +++ b/packages/@uppy/provider-views/src/style/uppy-ProviderBrowser-viewType--grid.scss @@ -142,7 +142,7 @@ } } // Checked: show the checkmark - .uppy-ProviderBrowserItem-checkbox--is-checked { + .uppy-ProviderBrowserItem--is-checked .uppy-ProviderBrowserItem-checkbox { opacity: 1; } diff --git a/packages/@uppy/provider-views/src/style/uppy-ProviderBrowser-viewType--list.scss b/packages/@uppy/provider-views/src/style/uppy-ProviderBrowser-viewType--list.scss index afd3c329dc..9ae70bd865 100644 --- a/packages/@uppy/provider-views/src/style/uppy-ProviderBrowser-viewType--list.scss +++ b/packages/@uppy/provider-views/src/style/uppy-ProviderBrowser-viewType--list.scss @@ -55,12 +55,14 @@ } } // Checked: color the background, show the checkmark - .uppy-ProviderBrowserItem-checkbox--is-checked, .uppy-ProviderBrowserItem-checkbox--is-partial { - background-color: $blue; - border-color: $blue; - - &::after { - opacity: 1; + .uppy-ProviderBrowserItem--is-checked, .uppy-ProviderBrowserItem--is-partial { + .uppy-ProviderBrowserItem-checkbox { + background-color: $blue; + border-color: $blue; + + &::after { + opacity: 1; + } } } diff --git a/packages/@uppy/provider-views/src/style/uppy-ProviderBrowserItem-checkbox.scss b/packages/@uppy/provider-views/src/style/uppy-ProviderBrowserItem-checkbox.scss index a8592182a3..fbabe463b2 100644 --- a/packages/@uppy/provider-views/src/style/uppy-ProviderBrowserItem-checkbox.scss +++ b/packages/@uppy/provider-views/src/style/uppy-ProviderBrowserItem-checkbox.scss @@ -17,7 +17,7 @@ } } -.uppy-ProviderBrowserItem-checkbox--is-checked { +.uppy-ProviderBrowserItem--is-checked .uppy-ProviderBrowserItem-checkbox { [data-uppy-theme='dark'] & { background-color: $gray-800; } @@ -34,7 +34,7 @@ } } -.uppy-ProviderBrowserItem-checkbox--is-partial{ +.uppy-ProviderBrowserItem--is-partial .uppy-ProviderBrowserItem-checkbox { &::after { content: '' !important; position: absolute !important; From 14b93b5b2179d0e88b2c79e0350e528907a2696b Mon Sep 17 00:00:00 2001 From: Evgenia Karunus Date: Mon, 8 Apr 2024 14:11:55 +0400 Subject: [PATCH 034/170] everywhere - refactor `.validateRestrictions()` --- packages/@uppy/provider-views/src/Browser.tsx | 14 ++++---------- packages/@uppy/provider-views/src/Item/index.tsx | 16 +++++++--------- .../src/ProviderView/ProviderView.tsx | 9 +++------ .../SearchProviderView/SearchProviderView.tsx | 6 +----- packages/@uppy/provider-views/src/View.ts | 15 +++++++++++++++ 5 files changed, 30 insertions(+), 30 deletions(-) diff --git a/packages/@uppy/provider-views/src/Browser.tsx b/packages/@uppy/provider-views/src/Browser.tsx index c67a9dfd45..045a67ba7a 100644 --- a/packages/@uppy/provider-views/src/Browser.tsx +++ b/packages/@uppy/provider-views/src/Browser.tsx @@ -12,11 +12,11 @@ import SearchFilterInput from './SearchFilterInput.tsx' import FooterActions from './FooterActions.tsx' import Item from './Item/index.tsx' import type { PartialTreeFile, PartialTreeFolderNode } from '@uppy/core/lib/Uppy.ts' +import type { RestrictionError } from '@uppy/core/lib/Restricter.ts' type BrowserProps = { displayedPartialTree: (PartialTreeFile | PartialTreeFolderNode)[], - currentSelection: (PartialTreeFile | PartialTreeFolderNode)[], - uppyFiles: UppyFile[] + viewType: string headerComponent?: JSX.Element showBreadcrumbs: boolean @@ -25,7 +25,7 @@ type BrowserProps = { handleScroll: (event: Event) => Promise showTitles: boolean i18n: I18n - validateRestrictions: Uppy['validateRestrictions'] + validateRestrictions: (file: PartialTreeFile | PartialTreeFolderNode) => RestrictionError | null isLoading: boolean | string showSearchFilter: boolean search: (query: string) => void @@ -46,7 +46,6 @@ function Browser( ): JSX.Element { const { displayedPartialTree, - uppyFiles, viewType, headerComponent, showBreadcrumbs, @@ -69,10 +68,9 @@ function Browser( done, noResultsLabel, loadAllFiles, - currentSelection } = props - const selected = currentSelection.length + const selected = 0; // TODO// currentSelection.length return (
            ( data={displayedPartialTree} renderRow={(file: PartialTreeFile | PartialTreeFolderNode) => ( ( > {displayedPartialTree.map((file) => ( = { - currentSelection: any[] - uppyFiles: UppyFile[] viewType: string toggleCheckbox: (event: Event, file: (PartialTreeFile | PartialTreeFolderNode)) => void recordShiftKeyPress: (event: KeyboardEvent | MouseEvent) => void showTitles: boolean i18n: I18n - validateRestrictions: Uppy['validateRestrictions'] + validateRestrictions: (file: PartialTreeFile | PartialTreeFolderNode) => RestrictionError | null getFolder: (folderId: PartialTreeId) => void file: PartialTreeFile | PartialTreeFolderNode } @@ -28,9 +26,9 @@ type ItemProps = { export default function Item( props: ItemProps, ): h.JSX.Element { - const { currentSelection, uppyFiles, viewType, toggleCheckbox, recordShiftKeyPress, showTitles, i18n, validateRestrictions, getFolder, file } = props + const { viewType, toggleCheckbox, recordShiftKeyPress, showTitles, i18n, validateRestrictions, getFolder, file } = props - const restrictionError = file.data.isFolder ? null : validateRestrictions(remoteFileObjToLocal(file.data), [...uppyFiles, ...currentSelection]) + const restrictionError = validateRestrictions(file) const isDisabled = file.data.isFolder ? false : (Boolean(restrictionError) && (file.status !== "checked")) const sharedProps = { @@ -52,6 +50,7 @@ export default function Item( ), itemIconEl: , isDisabled, + restrictionError } let ourProps = file.data.isFolder ? @@ -64,8 +63,7 @@ export default function Item( { ...sharedProps, isCheckboxDisabled: false, - type: 'file', - restrictionError, + type: 'file' } switch (viewType) { diff --git a/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx b/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx index a3123c8ac4..b8517dcae2 100644 --- a/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx +++ b/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx @@ -11,7 +11,7 @@ import type { PartialTreeFolderNode, PartialTreeFile, } from '@uppy/core/lib/Uppy.ts' -import type { Body, Meta } from '@uppy/utils/lib/UppyFile' +import type { Body, Meta, UppyFile } from '@uppy/utils/lib/UppyFile' import type { CompanionFile } from '@uppy/utils/lib/CompanionFile.ts' import type Translator from '@uppy/utils/lib/Translator' import type { DefinePluginOpts } from '@uppy/core/lib/BasePlugin.ts' @@ -632,7 +632,6 @@ export default class ProviderView extends View< searchOnInput: true, searchInputLabel: i18n('filter'), clearSearchLabel: i18n('resetFilter'), - currentSelection: partialTree.filter((item) => item.type !== 'root' && item.status === "checked") as (PartialTreeFile | PartialTreeFolderNode)[], noResultsLabel: i18n('noFilesFound'), logout: this.logout, @@ -647,10 +646,8 @@ export default class ProviderView extends View< showBreadcrumbs: targetViewOptions.showBreadcrumbs, pluginIcon, i18n: this.plugin.uppy.i18n, - uppyFiles: this.plugin.uppy.getFiles(), - validateRestrictions: ( - ...args: Parameters['validateRestrictions']> - ) => this.plugin.uppy.validateRestrictions(...args), + + validateRestrictions: this.validateRestrictions, isLoading: loading, } diff --git a/packages/@uppy/provider-views/src/SearchProviderView/SearchProviderView.tsx b/packages/@uppy/provider-views/src/SearchProviderView/SearchProviderView.tsx index b685ff8827..b44d8f703d 100644 --- a/packages/@uppy/provider-views/src/SearchProviderView/SearchProviderView.tsx +++ b/packages/@uppy/provider-views/src/SearchProviderView/SearchProviderView.tsx @@ -183,7 +183,6 @@ export default class SearchProviderView< const browserProps = { toggleCheckbox: this.toggleCheckbox.bind(this), recordShiftKeyPress, - currentSelection: partialTree.filter((i) => i.type !== 'root' && i.status === 'checked') as PartialTreeFile[], displayedPartialTree, handleScroll: this.handleScroll, done: this.donePicking, @@ -208,10 +207,7 @@ export default class SearchProviderView< showBreadcrumbs: targetViewOptions.showBreadcrumbs, pluginIcon: this.plugin.icon, i18n, - uppyFiles: this.plugin.uppy.getFiles(), - validateRestrictions: ( - ...args: Parameters['validateRestrictions']> - ) => this.plugin.uppy.validateRestrictions(...args), + validateRestrictions: this.validateRestrictions, } if (isInputMode) { diff --git a/packages/@uppy/provider-views/src/View.ts b/packages/@uppy/provider-views/src/View.ts index d590ea34f2..72153477d4 100644 --- a/packages/@uppy/provider-views/src/View.ts +++ b/packages/@uppy/provider-views/src/View.ts @@ -11,6 +11,7 @@ import type { CompanionFile } from '@uppy/utils/lib/CompanionFile' import getFileType from '@uppy/utils/lib/getFileType' import isPreviewSupported from '@uppy/utils/lib/isPreviewSupported' import remoteFileObjToLocal from '@uppy/utils/lib/remoteFileObjToLocal' +import type { RestrictionError } from '@uppy/core/lib/Restricter' type PluginType = 'Provider' | 'SearchProvider' @@ -70,6 +71,20 @@ export default class View< this.handleError = this.handleError.bind(this) this.clearSelection = this.clearSelection.bind(this) this.cancelPicking = this.cancelPicking.bind(this) + this.validateRestrictions = this.validateRestrictions.bind(this) + } + + validateRestrictions (file: PartialTreeFile | PartialTreeFolderNode) : RestrictionError | null { + if (file.data.isFolder) return null + + const localData = remoteFileObjToLocal(file.data) + + const { partialTree } = this.plugin.getPluginState() + const aleadyAddedFiles = this.plugin.uppy.getFiles() + const checkedFiles = partialTree.filter((item) => item.type === 'file' && item.status === 'checked') as PartialTreeFile[] + const checkedFilesData = checkedFiles.map((item) => item.data) + + return this.plugin.uppy.validateRestrictions(localData, [...aleadyAddedFiles, ...checkedFilesData]) } shouldHandleScroll(event: Event): boolean { From f021b518aff55c205618d15b02ac4e2caf6fd427 Mon Sep 17 00:00:00 2001 From: Evgenia Karunus Date: Mon, 8 Apr 2024 14:48:00 +0400 Subject: [PATCH 035/170] nOfSelectedFiles - make "Selected (n)" as smart as possible --- packages/@uppy/provider-views/src/Browser.tsx | 7 ++++--- .../src/ProviderView/ProviderView.tsx | 1 + .../SearchProviderView/SearchProviderView.tsx | 1 + packages/@uppy/provider-views/src/View.ts | 18 ++++++++++++++++++ 4 files changed, 24 insertions(+), 3 deletions(-) diff --git a/packages/@uppy/provider-views/src/Browser.tsx b/packages/@uppy/provider-views/src/Browser.tsx index 045a67ba7a..950642c8b5 100644 --- a/packages/@uppy/provider-views/src/Browser.tsx +++ b/packages/@uppy/provider-views/src/Browser.tsx @@ -26,6 +26,7 @@ type BrowserProps = { showTitles: boolean i18n: I18n validateRestrictions: (file: PartialTreeFile | PartialTreeFolderNode) => RestrictionError | null + getNOfSelectedFiles: () => number isLoading: boolean | string showSearchFilter: boolean search: (query: string) => void @@ -70,7 +71,7 @@ function Browser( loadAllFiles, } = props - const selected = 0; // TODO// currentSelection.length + const nOfSelectedFiles = props.getNOfSelectedFiles(); // TODO// currentSelection.length return (
            ( ) })()} - {selected > 0 && ( + {nOfSelectedFiles > 0 && ( extends View< i18n: this.plugin.uppy.i18n, validateRestrictions: this.validateRestrictions, + getNOfSelectedFiles: this.getNOfSelectedFiles, isLoading: loading, } diff --git a/packages/@uppy/provider-views/src/SearchProviderView/SearchProviderView.tsx b/packages/@uppy/provider-views/src/SearchProviderView/SearchProviderView.tsx index b44d8f703d..b2b4a1a842 100644 --- a/packages/@uppy/provider-views/src/SearchProviderView/SearchProviderView.tsx +++ b/packages/@uppy/provider-views/src/SearchProviderView/SearchProviderView.tsx @@ -208,6 +208,7 @@ export default class SearchProviderView< pluginIcon: this.plugin.icon, i18n, validateRestrictions: this.validateRestrictions, + getNOfSelectedFiles: this.getNOfSelectedFiles } if (isInputMode) { diff --git a/packages/@uppy/provider-views/src/View.ts b/packages/@uppy/provider-views/src/View.ts index 72153477d4..b685f1caf6 100644 --- a/packages/@uppy/provider-views/src/View.ts +++ b/packages/@uppy/provider-views/src/View.ts @@ -72,6 +72,24 @@ export default class View< this.clearSelection = this.clearSelection.bind(this) this.cancelPicking = this.cancelPicking.bind(this) this.validateRestrictions = this.validateRestrictions.bind(this) + this.getNOfSelectedFiles = this.getNOfSelectedFiles.bind(this) + } + + getNOfSelectedFiles () : number { + const { partialTree } = this.plugin.getPluginState() + // We're interested in all 'checked' leaves. + const checkedLeaves = partialTree.filter((item) => { + if (item.type === 'file' && item.status === 'checked') { + return true + } else if (item.type === 'folder' && item.status === 'checked') { + const doesItHaveChildren = partialTree.some((i) => + i.type !== 'root' && i.parentId === item.id + ) + return !doesItHaveChildren + } + return false + }) + return checkedLeaves.length } validateRestrictions (file: PartialTreeFile | PartialTreeFolderNode) : RestrictionError | null { From 196be2226ebc4a373349b93139c0959b6f1ba854 Mon Sep 17 00:00:00 2001 From: Evgenia Karunus Date: Tue, 9 Apr 2024 11:02:44 +0400 Subject: [PATCH 036/170] - prevent shift-clicking from highlighting file names --- packages/@uppy/provider-views/src/View.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/@uppy/provider-views/src/View.ts b/packages/@uppy/provider-views/src/View.ts index b685f1caf6..a2f2cd3440 100644 --- a/packages/@uppy/provider-views/src/View.ts +++ b/packages/@uppy/provider-views/src/View.ts @@ -239,6 +239,8 @@ export default class View< toggleCheckbox(e: Event, ourItem: PartialTreeFolderNode | PartialTreeFile) { e.stopPropagation() e.preventDefault() + // Prevent shift-clicking from highlighting file names (https://stackoverflow.com/a/1527797/3192470) + document.getSelection()?.removeAllRanges() const { partialTree, currentFolderId } = this.plugin.getPluginState() const newPartialTree : PartialTree = JSON.parse(JSON.stringify(partialTree)) From 4c1bef74d854ca2121f22f0edaff2d0bf8d3952e Mon Sep 17 00:00:00 2001 From: Evgenia Karunus Date: Tue, 9 Apr 2024 14:20:47 +0400 Subject: [PATCH 037/170] `.validateRestrictions()` - make it accept a `CompanionFile` instead of `PartialTree`'s file --- packages/@uppy/provider-views/src/Browser.tsx | 3 ++- packages/@uppy/provider-views/src/Item/index.tsx | 5 +++-- packages/@uppy/provider-views/src/View.ts | 6 +++--- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/packages/@uppy/provider-views/src/Browser.tsx b/packages/@uppy/provider-views/src/Browser.tsx index 950642c8b5..e21de49436 100644 --- a/packages/@uppy/provider-views/src/Browser.tsx +++ b/packages/@uppy/provider-views/src/Browser.tsx @@ -13,6 +13,7 @@ import FooterActions from './FooterActions.tsx' import Item from './Item/index.tsx' import type { PartialTreeFile, PartialTreeFolderNode } from '@uppy/core/lib/Uppy.ts' import type { RestrictionError } from '@uppy/core/lib/Restricter.ts' +import type { CompanionFile } from '@uppy/utils/lib/CompanionFile' type BrowserProps = { displayedPartialTree: (PartialTreeFile | PartialTreeFolderNode)[], @@ -25,7 +26,7 @@ type BrowserProps = { handleScroll: (event: Event) => Promise showTitles: boolean i18n: I18n - validateRestrictions: (file: PartialTreeFile | PartialTreeFolderNode) => RestrictionError | null + validateRestrictions: (file: CompanionFile) => RestrictionError | null getNOfSelectedFiles: () => number isLoading: boolean | string showSearchFilter: boolean diff --git a/packages/@uppy/provider-views/src/Item/index.tsx b/packages/@uppy/provider-views/src/Item/index.tsx index a07a9feab5..cec933423f 100644 --- a/packages/@uppy/provider-views/src/Item/index.tsx +++ b/packages/@uppy/provider-views/src/Item/index.tsx @@ -9,6 +9,7 @@ import GridListItem from './components/GridLi.tsx' import ListItem from './components/ListLi.tsx' import type { PartialTreeFile, PartialTreeFolderNode, PartialTreeId, Uppy } from '@uppy/core/lib/Uppy.ts' import type { RestrictionError } from '@uppy/core/lib/Restricter.ts' +import type { CompanionFile } from '@uppy/utils/lib/CompanionFile' const VIRTUAL_SHARED_DIR = 'shared-with-me' @@ -18,7 +19,7 @@ type ItemProps = { recordShiftKeyPress: (event: KeyboardEvent | MouseEvent) => void showTitles: boolean i18n: I18n - validateRestrictions: (file: PartialTreeFile | PartialTreeFolderNode) => RestrictionError | null + validateRestrictions: (file: CompanionFile) => RestrictionError | null getFolder: (folderId: PartialTreeId) => void file: PartialTreeFile | PartialTreeFolderNode } @@ -28,7 +29,7 @@ export default function Item( ): h.JSX.Element { const { viewType, toggleCheckbox, recordShiftKeyPress, showTitles, i18n, validateRestrictions, getFolder, file } = props - const restrictionError = validateRestrictions(file) + const restrictionError = validateRestrictions(file.data) const isDisabled = file.data.isFolder ? false : (Boolean(restrictionError) && (file.status !== "checked")) const sharedProps = { diff --git a/packages/@uppy/provider-views/src/View.ts b/packages/@uppy/provider-views/src/View.ts index a2f2cd3440..a816b552e9 100644 --- a/packages/@uppy/provider-views/src/View.ts +++ b/packages/@uppy/provider-views/src/View.ts @@ -92,10 +92,10 @@ export default class View< return checkedLeaves.length } - validateRestrictions (file: PartialTreeFile | PartialTreeFolderNode) : RestrictionError | null { - if (file.data.isFolder) return null + validateRestrictions (file: CompanionFile) : RestrictionError | null { + if (file.isFolder) return null - const localData = remoteFileObjToLocal(file.data) + const localData = remoteFileObjToLocal(file) const { partialTree } = this.plugin.getPluginState() const aleadyAddedFiles = this.plugin.uppy.getFiles() From fe1ac0dce1edde39a6cb3c2131daa2c3403b6c7a Mon Sep 17 00:00:00 2001 From: Evgenia Karunus Date: Wed, 10 Apr 2024 12:01:07 +0400 Subject: [PATCH 038/170] `.getFolder()` - simplify code --- .../src/ProviderView/ProviderView.tsx | 93 +++++++++---------- 1 file changed, 43 insertions(+), 50 deletions(-) diff --git a/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx b/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx index 8dd4a22208..71a0eecaaa 100644 --- a/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx +++ b/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx @@ -178,9 +178,8 @@ export default class ProviderView extends View< console.log(`____________________________________________GETTING FOLDER "${folderId}"`); // Returning cached folder const { partialTree } = this.plugin.getPluginState() - const clickedFolder = partialTree.find((folder) => folder.id === folderId) as PartialTreeFolder | undefined - const thisFolderIsCached = clickedFolder && clickedFolder.cached - if (thisFolderIsCached) { + const clickedFolder = partialTree.find((folder) => folder.id === folderId)! as PartialTreeFolder + if (clickedFolder.cached) { console.log("Folder was cached____________________________________________"); this.plugin.setPluginState({ currentFolderId: folderId, filterInput: '' }) return @@ -205,56 +204,50 @@ export default class ProviderView extends View< let newFolders = currentItems.filter((i) => i.isFolder === true) let newFiles = currentItems.filter((i) => i.isFolder === false) - const clickedFolder : PartialTreeFolder = partialTree.find((folder) => folder.id === folderId)! as PartialTreeFolder + const newlyAddedItemStatus = (clickedFolder.type === 'folder' && clickedFolder.status === 'checked') ? 'checked' : 'unchecked'; + const folders : PartialTreeFolderNode[] = newFolders.map((folder) => ({ + type: 'folder', + id: folder.requestPath, - // If selected folder is already filled in, don't refill it (because that would make it lose deep state!) - // Otherwise, cache the current folder! - if (clickedFolder && !clickedFolder.cached) { - const newlyAddedItemStatus = (clickedFolder.type === 'folder' && clickedFolder.status === 'checked') ? 'checked' : 'unchecked'; - const folders : PartialTreeFolderNode[] = newFolders.map((folder) => ({ - type: 'folder', - id: folder.requestPath, - - cached: false, - nextPagePath: null, - - status: newlyAddedItemStatus, - parentId: clickedFolder.id, - data: folder, - })) - const files : PartialTreeFile[] = newFiles.map((file) => ({ - type: 'file', - id: file.requestPath, - - status: newlyAddedItemStatus, - parentId: clickedFolder.id, - data: file, - })) - - // just doing `clickedFolder.cached = true` in a non-mutating way - const updatedClickedFolder : PartialTreeFolder = { - ...clickedFolder, - cached: true, - nextPagePath: currentPagePath - } - const partialTreeWithUpdatedClickedFolder = partialTree.map((folder) => - folder.id === updatedClickedFolder.id ? - updatedClickedFolder : - folder - ) + cached: false, + nextPagePath: null, + + status: newlyAddedItemStatus, + parentId: clickedFolder.id, + data: folder, + })) + const files : PartialTreeFile[] = newFiles.map((file) => ({ + type: 'file', + id: file.requestPath, + + status: newlyAddedItemStatus, + parentId: clickedFolder.id, + data: file, + })) + + // just doing `clickedFolder.cached = true` in a non-mutating way + const updatedClickedFolder : PartialTreeFolder = { + ...clickedFolder, + cached: true, + nextPagePath: currentPagePath + } + const partialTreeWithUpdatedClickedFolder = partialTree.map((folder) => + folder.id === updatedClickedFolder.id ? + updatedClickedFolder : + folder + ) - const newPartialTree = [ - ...partialTreeWithUpdatedClickedFolder, - ...folders, - ...files - ] + const newPartialTree = [ + ...partialTreeWithUpdatedClickedFolder, + ...folders, + ...files + ] - this.plugin.setPluginState({ - partialTree: newPartialTree, - currentFolderId: folderId, - filterInput: '' - }) - } + this.plugin.setPluginState({ + partialTree: newPartialTree, + currentFolderId: folderId, + filterInput: '' + }) }) From fbbaa1386e014006734b946a02c8948317547187 Mon Sep 17 00:00:00 2001 From: Evgenia Karunus Date: Wed, 10 Apr 2024 13:38:22 +0400 Subject: [PATCH 039/170] everywhere - account for `restrictions` in `.partialTree` --- .../src/ProviderView/ProviderView.tsx | 4 ++-- packages/@uppy/provider-views/src/View.ts | 21 +++++++++++++++---- 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx b/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx index 71a0eecaaa..b12fc6f07e 100644 --- a/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx +++ b/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx @@ -220,7 +220,7 @@ export default class ProviderView extends View< type: 'file', id: file.requestPath, - status: newlyAddedItemStatus, + status: newlyAddedItemStatus === 'checked' && this.validateRestrictions(file) ? 'unchecked' : newlyAddedItemStatus, parentId: clickedFolder.id, data: file, })) @@ -387,7 +387,7 @@ export default class ProviderView extends View< type: 'file', id: file.requestPath, - status: newlyAddedItemStatus, + status: newlyAddedItemStatus === 'checked' && this.validateRestrictions(file) ? 'unchecked' : newlyAddedItemStatus, parentId: scrolledFolder.id, data: file, })) diff --git a/packages/@uppy/provider-views/src/View.ts b/packages/@uppy/provider-views/src/View.ts index a816b552e9..f9457d6167 100644 --- a/packages/@uppy/provider-views/src/View.ts +++ b/packages/@uppy/provider-views/src/View.ts @@ -250,7 +250,11 @@ export default class View< const percolateDown = (clickedItem: PartialTreeFolderNode | PartialTreeFile, status: 'checked' | 'unchecked') => { const children = newPartialTree.filter((item) => item.type !== 'root' && item.parentId === clickedItem.id) as (PartialTreeFolderNode | PartialTreeFile)[] children.forEach((item) => { - item.status = status + if (item.type === 'file') { + item.status = status === 'checked' && this.validateRestrictions(item.data) ? 'unchecked' : status + } else { + item.status = status + } percolateDown(item, status) }) } @@ -260,8 +264,11 @@ export default class View< if (parentFolder.type === 'root') return const parentsChildren = newPartialTree.filter((item) => item.type !== 'root' && item.parentId === parentFolder.id) as (PartialTreeFile | PartialTreeFolderNode)[] - const areAllChildrenChecked = parentsChildren.every((item) => item.status === "checked") - const areAllChildrenUnchecked = parentsChildren.every((item) => item.status === "unchecked") + const parentsValidChildren = parentsChildren.filter((item) => + !this.validateRestrictions(item.data) + ) + const areAllChildrenChecked = parentsValidChildren.every((item) => item.status === "checked") + const areAllChildrenUnchecked = parentsValidChildren.every((item) => item.status === "unchecked") if (areAllChildrenChecked) { parentFolder.status = "checked" @@ -288,7 +295,13 @@ export default class View< const newlyCheckedItems = newPartialTree .filter((item) => item.type !== 'root' && toMarkAsChecked.includes(item.id)) as (PartialTreeFile | PartialTreeFolderNode)[] - newlyCheckedItems.forEach((item) => item.status = 'checked') + newlyCheckedItems.forEach((item) => { + if (item.type === 'file') { + item.status = this.validateRestrictions(item.data) ? 'unchecked' : 'checked' + } else { + item.status = 'checked' + } + }) newlyCheckedItems.forEach((item) => { percolateDown(item, 'checked') From 095c900a39dafc8425158fb5830c2422aadf51bd Mon Sep 17 00:00:00 2001 From: Evgenia Karunus Date: Wed, 10 Apr 2024 14:31:18 +0400 Subject: [PATCH 040/170] `PartialTreeUtils.ts` - factor out `getPartialTreeAfterTogglingCheckboxes()` --- packages/@uppy/provider-views/src/View.ts | 71 +-------------- .../src/utils/PartialTreeUtils.ts | 87 +++++++++++++++++++ 2 files changed, 89 insertions(+), 69 deletions(-) create mode 100644 packages/@uppy/provider-views/src/utils/PartialTreeUtils.ts diff --git a/packages/@uppy/provider-views/src/View.ts b/packages/@uppy/provider-views/src/View.ts index f9457d6167..eccbc6aa4a 100644 --- a/packages/@uppy/provider-views/src/View.ts +++ b/packages/@uppy/provider-views/src/View.ts @@ -12,6 +12,7 @@ import getFileType from '@uppy/utils/lib/getFileType' import isPreviewSupported from '@uppy/utils/lib/isPreviewSupported' import remoteFileObjToLocal from '@uppy/utils/lib/remoteFileObjToLocal' import type { RestrictionError } from '@uppy/core/lib/Restricter' +import PartialTreeUtils from './utils/PartialTreeUtils' type PluginType = 'Provider' | 'SearchProvider' @@ -243,76 +244,8 @@ export default class View< document.getSelection()?.removeAllRanges() const { partialTree, currentFolderId } = this.plugin.getPluginState() - const newPartialTree : PartialTree = JSON.parse(JSON.stringify(partialTree)) - - // if newStatus is "checked" - percolate down "checked" - // if newStatus is "unchecked" - percolate down "unchecked" - const percolateDown = (clickedItem: PartialTreeFolderNode | PartialTreeFile, status: 'checked' | 'unchecked') => { - const children = newPartialTree.filter((item) => item.type !== 'root' && item.parentId === clickedItem.id) as (PartialTreeFolderNode | PartialTreeFile)[] - children.forEach((item) => { - if (item.type === 'file') { - item.status = status === 'checked' && this.validateRestrictions(item.data) ? 'unchecked' : status - } else { - item.status = status - } - percolateDown(item, status) - }) - } - // we do something to all of its parents. - const percolateUp = (currentItem: PartialTreeFolderNode | PartialTreeFile) => { - const parentFolder = newPartialTree.find((item) => item.id === currentItem.parentId)! as PartialTreeFolder - if (parentFolder.type === 'root') return - - const parentsChildren = newPartialTree.filter((item) => item.type !== 'root' && item.parentId === parentFolder.id) as (PartialTreeFile | PartialTreeFolderNode)[] - const parentsValidChildren = parentsChildren.filter((item) => - !this.validateRestrictions(item.data) - ) - const areAllChildrenChecked = parentsValidChildren.every((item) => item.status === "checked") - const areAllChildrenUnchecked = parentsValidChildren.every((item) => item.status === "unchecked") - - if (areAllChildrenChecked) { - parentFolder.status = "checked" - } else if (areAllChildrenUnchecked) { - parentFolder.status = "unchecked" - } else { - parentFolder.status = "partial" - } - percolateUp(parentFolder) - } - - // Shift-clicking selects a single consecutive list of items - // starting at the previous click. - const inThisFolder = this.filterItems(newPartialTree.filter((item) => item.type !== 'root' && item.parentId === currentFolderId)) as (PartialTreeFile | PartialTreeFolderNode)[] - const prevIndex = inThisFolder.findIndex((item) => item.id === this.lastCheckbox) - if (prevIndex !== -1 && this.isShiftKeyPressed) { - const newIndex = inThisFolder.findIndex((item) => item.id === ourItem.id) - const toMarkAsChecked = (prevIndex < newIndex ? - inThisFolder.slice(prevIndex, newIndex + 1) - : inThisFolder.slice(newIndex, prevIndex + 1) - ).map((item) => item.id) - - const newlyCheckedItems = newPartialTree - .filter((item) => item.type !== 'root' && toMarkAsChecked.includes(item.id)) as (PartialTreeFile | PartialTreeFolderNode)[] - - newlyCheckedItems.forEach((item) => { - if (item.type === 'file') { - item.status = this.validateRestrictions(item.data) ? 'unchecked' : 'checked' - } else { - item.status = 'checked' - } - }) - - newlyCheckedItems.forEach((item) => { - percolateDown(item, 'checked') - }) - percolateUp(ourItem) - } else { - const ourItemInNewTree = newPartialTree.find((item) => item.id === ourItem.id) as (PartialTreeFile | PartialTreeFolderNode) - ourItemInNewTree.status = ourItem.status === 'checked' ? 'unchecked' : 'checked' - percolateDown(ourItem, ourItemInNewTree.status) - percolateUp(ourItem) - } + const newPartialTree = PartialTreeUtils.getPartialTreeAfterTogglingCheckboxes(partialTree, ourItem, this.validateRestrictions, this.filterItems, currentFolderId, this.isShiftKeyPressed, this.lastCheckbox) this.plugin.setPluginState({ partialTree: newPartialTree }) this.lastCheckbox = ourItem.id! diff --git a/packages/@uppy/provider-views/src/utils/PartialTreeUtils.ts b/packages/@uppy/provider-views/src/utils/PartialTreeUtils.ts new file mode 100644 index 0000000000..dd38a445fa --- /dev/null +++ b/packages/@uppy/provider-views/src/utils/PartialTreeUtils.ts @@ -0,0 +1,87 @@ +import type { PartialTree, PartialTreeFile, PartialTreeFolder, PartialTreeFolderNode } from "@uppy/core/lib/Uppy" +import type { CompanionFile } from "@uppy/utils/lib/CompanionFile" + +const getPartialTreeAfterTogglingCheckboxes = ( + oldPartialTree: PartialTree, + ourItem: PartialTreeFolderNode | PartialTreeFile, + validateRestrictions: (file: CompanionFile) => object | null, + filterItems : (items: PartialTree) => PartialTree, + currentFolderId: string | null, + isShiftKeyPressed: boolean, + lastCheckbox: string | undefined +) : PartialTree => { + const newPartialTree : PartialTree = JSON.parse(JSON.stringify(oldPartialTree)) + + // if newStatus is "checked" - percolate down "checked" + // if newStatus is "unchecked" - percolate down "unchecked" + const percolateDown = (clickedItem: PartialTreeFolderNode | PartialTreeFile, status: 'checked' | 'unchecked') => { + const children = newPartialTree.filter((item) => item.type !== 'root' && item.parentId === clickedItem.id) as (PartialTreeFolderNode | PartialTreeFile)[] + children.forEach((item) => { + if (item.type === 'file') { + item.status = status === 'checked' && validateRestrictions(item.data) ? 'unchecked' : status + } else { + item.status = status + } + percolateDown(item, status) + }) + } + // we do something to all of its parents. + const percolateUp = (currentItem: PartialTreeFolderNode | PartialTreeFile) => { + const parentFolder = newPartialTree.find((item) => item.id === currentItem.parentId)! as PartialTreeFolder + if (parentFolder.type === 'root') return + + const parentsChildren = newPartialTree.filter((item) => item.type !== 'root' && item.parentId === parentFolder.id) as (PartialTreeFile | PartialTreeFolderNode)[] + const parentsValidChildren = parentsChildren.filter((item) => + !validateRestrictions(item.data) + ) + const areAllChildrenChecked = parentsValidChildren.every((item) => item.status === "checked") + const areAllChildrenUnchecked = parentsValidChildren.every((item) => item.status === "unchecked") + + if (areAllChildrenChecked) { + parentFolder.status = "checked" + } else if (areAllChildrenUnchecked) { + parentFolder.status = "unchecked" + } else { + parentFolder.status = "partial" + } + + percolateUp(parentFolder) + } + + // Shift-clicking selects a single consecutive list of items + // starting at the previous click. + const inThisFolder = filterItems(newPartialTree.filter((item) => item.type !== 'root' && item.parentId === currentFolderId)) as (PartialTreeFile | PartialTreeFolderNode)[] + const prevIndex = inThisFolder.findIndex((item) => item.id === lastCheckbox) + if (prevIndex !== -1 && isShiftKeyPressed) { + const newIndex = inThisFolder.findIndex((item) => item.id === ourItem.id) + const toMarkAsChecked = (prevIndex < newIndex ? + inThisFolder.slice(prevIndex, newIndex + 1) + : inThisFolder.slice(newIndex, prevIndex + 1) + ).map((item) => item.id) + + const newlyCheckedItems = newPartialTree + .filter((item) => item.type !== 'root' && toMarkAsChecked.includes(item.id)) as (PartialTreeFile | PartialTreeFolderNode)[] + + newlyCheckedItems.forEach((item) => { + if (item.type === 'file') { + item.status = validateRestrictions(item.data) ? 'unchecked' : 'checked' + } else { + item.status = 'checked' + } + }) + + newlyCheckedItems.forEach((item) => { + percolateDown(item, 'checked') + }) + percolateUp(ourItem) + } else { + const ourItemInNewTree = newPartialTree.find((item) => item.id === ourItem.id) as (PartialTreeFile | PartialTreeFolderNode) + ourItemInNewTree.status = ourItem.status === 'checked' ? 'unchecked' : 'checked' + percolateDown(ourItem, ourItemInNewTree.status) + percolateUp(ourItem) + } + + return newPartialTree +} + +export default { getPartialTreeAfterTogglingCheckboxes } From 9759e7629aa602e0971102a252c10408c801bc92 Mon Sep 17 00:00:00 2001 From: Evgenia Karunus Date: Wed, 10 Apr 2024 14:43:39 +0400 Subject: [PATCH 041/170] `PartialTreeUtils.ts` - factor out `clickOnFolder()` --- .../src/ProviderView/ProviderView.tsx | 43 +-------------- .../src/utils/PartialTreeUtils.ts | 53 ++++++++++++++++++- 2 files changed, 54 insertions(+), 42 deletions(-) diff --git a/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx b/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx index b12fc6f07e..2dd20028fd 100644 --- a/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx +++ b/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx @@ -24,6 +24,7 @@ import View, { type ViewOptions } from '../View.ts' // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore We don't want TS to generate types for the package.json import packageJson from '../../package.json' +import PartialTreeUtils from '../utils/PartialTreeUtils.ts' function formatBreadcrumbs(breadcrumbs: PartialTreeFolder[]): string { const nonrootFoldes = breadcrumbs @@ -201,47 +202,7 @@ export default class ProviderView extends View< this.setLoading(this.plugin.uppy.i18n('loadedXFiles', { numFiles: items.length })) } while (this.opts.loadAllFiles && currentPagePath) - let newFolders = currentItems.filter((i) => i.isFolder === true) - let newFiles = currentItems.filter((i) => i.isFolder === false) - - const newlyAddedItemStatus = (clickedFolder.type === 'folder' && clickedFolder.status === 'checked') ? 'checked' : 'unchecked'; - const folders : PartialTreeFolderNode[] = newFolders.map((folder) => ({ - type: 'folder', - id: folder.requestPath, - - cached: false, - nextPagePath: null, - - status: newlyAddedItemStatus, - parentId: clickedFolder.id, - data: folder, - })) - const files : PartialTreeFile[] = newFiles.map((file) => ({ - type: 'file', - id: file.requestPath, - - status: newlyAddedItemStatus === 'checked' && this.validateRestrictions(file) ? 'unchecked' : newlyAddedItemStatus, - parentId: clickedFolder.id, - data: file, - })) - - // just doing `clickedFolder.cached = true` in a non-mutating way - const updatedClickedFolder : PartialTreeFolder = { - ...clickedFolder, - cached: true, - nextPagePath: currentPagePath - } - const partialTreeWithUpdatedClickedFolder = partialTree.map((folder) => - folder.id === updatedClickedFolder.id ? - updatedClickedFolder : - folder - ) - - const newPartialTree = [ - ...partialTreeWithUpdatedClickedFolder, - ...folders, - ...files - ] + const newPartialTree = PartialTreeUtils.clickOnFolder(partialTree, currentItems, clickedFolder, this.validateRestrictions, currentPagePath) this.plugin.setPluginState({ partialTree: newPartialTree, diff --git a/packages/@uppy/provider-views/src/utils/PartialTreeUtils.ts b/packages/@uppy/provider-views/src/utils/PartialTreeUtils.ts index dd38a445fa..0da1270248 100644 --- a/packages/@uppy/provider-views/src/utils/PartialTreeUtils.ts +++ b/packages/@uppy/provider-views/src/utils/PartialTreeUtils.ts @@ -84,4 +84,55 @@ const getPartialTreeAfterTogglingCheckboxes = ( return newPartialTree } -export default { getPartialTreeAfterTogglingCheckboxes } +const clickOnFolder = ( + oldPartialTree: PartialTree, + currentItems: CompanionFile[], + clickedFolder: PartialTreeFolder, + validateRestrictions: (file: CompanionFile) => object | null, + currentPagePath: string | null +) : PartialTree => { + let newFolders = currentItems.filter((i) => i.isFolder === true) + let newFiles = currentItems.filter((i) => i.isFolder === false) + + const newlyAddedItemStatus = (clickedFolder.type === 'folder' && clickedFolder.status === 'checked') ? 'checked' : 'unchecked'; + const folders : PartialTreeFolderNode[] = newFolders.map((folder) => ({ + type: 'folder', + id: folder.requestPath, + + cached: false, + nextPagePath: null, + + status: newlyAddedItemStatus, + parentId: clickedFolder.id, + data: folder, + })) + const files : PartialTreeFile[] = newFiles.map((file) => ({ + type: 'file', + id: file.requestPath, + + status: newlyAddedItemStatus === 'checked' && validateRestrictions(file) ? 'unchecked' : newlyAddedItemStatus, + parentId: clickedFolder.id, + data: file, + })) + + // just doing `clickedFolder.cached = true` in a non-mutating way + const updatedClickedFolder : PartialTreeFolder = { + ...clickedFolder, + cached: true, + nextPagePath: currentPagePath + } + const partialTreeWithUpdatedClickedFolder = oldPartialTree.map((folder) => + folder.id === updatedClickedFolder.id ? + updatedClickedFolder : + folder + ) + + const newPartialTree = [ + ...partialTreeWithUpdatedClickedFolder, + ...folders, + ...files + ] + return newPartialTree +} + +export default { getPartialTreeAfterTogglingCheckboxes, clickOnFolder } From 56e5284c738bdb9c87e788a7029922066187a752 Mon Sep 17 00:00:00 2001 From: Evgenia Karunus Date: Wed, 10 Apr 2024 15:05:35 +0400 Subject: [PATCH 042/170] `PartialTreeUtils.ts` - factor out `getPartialTreeAfterScroll()` --- .../src/ProviderView/ProviderView.tsx | 35 +------------- .../src/utils/PartialTreeUtils.ts | 48 ++++++++++++++++++- 2 files changed, 48 insertions(+), 35 deletions(-) diff --git a/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx b/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx index 2dd20028fd..9dfa4d274e 100644 --- a/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx +++ b/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx @@ -324,40 +324,7 @@ export default class ProviderView extends View< requestPath: currentFolder.nextPagePath!, signal }) - let newFolders = items.filter((i) => i.isFolder === true) - let newFiles = items.filter((i) => i.isFolder === false) - - // just doing `scrolledFolder.nextPagePath = ...` in a non-mutating way - const scrolledFolder : PartialTreeFolder = {...currentFolder, nextPagePath } - const partialTreeWithUpdatedScrolledFolder = partialTree.map((folder) => - folder.id === scrolledFolder.id ? scrolledFolder : folder - ) - const newlyAddedItemStatus = (scrolledFolder.type === 'folder' && scrolledFolder.status === 'checked') ? 'checked' : 'unchecked'; - const folders : PartialTreeFolderNode[] = newFolders.map((folder) => ({ - type: 'folder', - id: folder.requestPath, - - cached: false, - nextPagePath: null, - - status: newlyAddedItemStatus, - parentId: scrolledFolder.id, - data: folder, - })) - const files : PartialTreeFile[] = newFiles.map((file) => ({ - type: 'file', - id: file.requestPath, - - status: newlyAddedItemStatus === 'checked' && this.validateRestrictions(file) ? 'unchecked' : newlyAddedItemStatus, - parentId: scrolledFolder.id, - data: file, - })) - - const newPartialTree : PartialTree = [ - ...partialTreeWithUpdatedScrolledFolder, - ...folders, - ...files - ] + const newPartialTree = PartialTreeUtils.getPartialTreeAfterScroll(partialTree, currentFolderId, items, nextPagePath, this.validateRestrictions) this.plugin.setPluginState({ partialTree: newPartialTree }) }) diff --git a/packages/@uppy/provider-views/src/utils/PartialTreeUtils.ts b/packages/@uppy/provider-views/src/utils/PartialTreeUtils.ts index 0da1270248..ac81e582b4 100644 --- a/packages/@uppy/provider-views/src/utils/PartialTreeUtils.ts +++ b/packages/@uppy/provider-views/src/utils/PartialTreeUtils.ts @@ -135,4 +135,50 @@ const clickOnFolder = ( return newPartialTree } -export default { getPartialTreeAfterTogglingCheckboxes, clickOnFolder } +const getPartialTreeAfterScroll = ( + oldPartialTree: PartialTree, + currentFolderId: string | null, + items: CompanionFile[], + nextPagePath: string | null, + validateRestrictions: (file: CompanionFile) => object | null, +) : PartialTree => { + const currentFolder = oldPartialTree.find((i) => i.id === currentFolderId) as PartialTreeFolder + + let newFolders = items.filter((i) => i.isFolder === true) + let newFiles = items.filter((i) => i.isFolder === false) + + // just doing `scrolledFolder.nextPagePath = ...` in a non-mutating way + const scrolledFolder : PartialTreeFolder = { ...currentFolder, nextPagePath } + const partialTreeWithUpdatedScrolledFolder = oldPartialTree.map((folder) => + folder.id === scrolledFolder.id ? scrolledFolder : folder + ) + const newlyAddedItemStatus = (scrolledFolder.type === 'folder' && scrolledFolder.status === 'checked') ? 'checked' : 'unchecked'; + const folders : PartialTreeFolderNode[] = newFolders.map((folder) => ({ + type: 'folder', + id: folder.requestPath, + + cached: false, + nextPagePath: null, + + status: newlyAddedItemStatus, + parentId: scrolledFolder.id, + data: folder, + })) + const files : PartialTreeFile[] = newFiles.map((file) => ({ + type: 'file', + id: file.requestPath, + + status: newlyAddedItemStatus === 'checked' && validateRestrictions(file) ? 'unchecked' : newlyAddedItemStatus, + parentId: scrolledFolder.id, + data: file, + })) + + const newPartialTree : PartialTree = [ + ...partialTreeWithUpdatedScrolledFolder, + ...folders, + ...files + ] + return newPartialTree +} + +export default { getPartialTreeAfterTogglingCheckboxes, clickOnFolder, getPartialTreeAfterScroll } From 3408c5fb6b3a7fbe52823f6924afaf1daeb08a58 Mon Sep 17 00:00:00 2001 From: Evgenia Karunus Date: Wed, 10 Apr 2024 15:12:42 +0400 Subject: [PATCH 043/170] `PartialTreeUtils.ts` - rename methods --- .../provider-views/src/ProviderView/ProviderView.tsx | 4 ++-- packages/@uppy/provider-views/src/View.ts | 2 +- .../@uppy/provider-views/src/utils/PartialTreeUtils.ts | 8 ++++---- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx b/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx index 9dfa4d274e..c6db41d405 100644 --- a/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx +++ b/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx @@ -202,7 +202,7 @@ export default class ProviderView extends View< this.setLoading(this.plugin.uppy.i18n('loadedXFiles', { numFiles: items.length })) } while (this.opts.loadAllFiles && currentPagePath) - const newPartialTree = PartialTreeUtils.clickOnFolder(partialTree, currentItems, clickedFolder, this.validateRestrictions, currentPagePath) + const newPartialTree = PartialTreeUtils.afterClickOnFolder(partialTree, currentItems, clickedFolder, this.validateRestrictions, currentPagePath) this.plugin.setPluginState({ partialTree: newPartialTree, @@ -324,7 +324,7 @@ export default class ProviderView extends View< requestPath: currentFolder.nextPagePath!, signal }) - const newPartialTree = PartialTreeUtils.getPartialTreeAfterScroll(partialTree, currentFolderId, items, nextPagePath, this.validateRestrictions) + const newPartialTree = PartialTreeUtils.afterScroll(partialTree, currentFolderId, items, nextPagePath, this.validateRestrictions) this.plugin.setPluginState({ partialTree: newPartialTree }) }) diff --git a/packages/@uppy/provider-views/src/View.ts b/packages/@uppy/provider-views/src/View.ts index eccbc6aa4a..fadafbd08e 100644 --- a/packages/@uppy/provider-views/src/View.ts +++ b/packages/@uppy/provider-views/src/View.ts @@ -245,7 +245,7 @@ export default class View< const { partialTree, currentFolderId } = this.plugin.getPluginState() - const newPartialTree = PartialTreeUtils.getPartialTreeAfterTogglingCheckboxes(partialTree, ourItem, this.validateRestrictions, this.filterItems, currentFolderId, this.isShiftKeyPressed, this.lastCheckbox) + const newPartialTree = PartialTreeUtils.afterToggleCheckbox(partialTree, ourItem, this.validateRestrictions, this.filterItems, currentFolderId, this.isShiftKeyPressed, this.lastCheckbox) this.plugin.setPluginState({ partialTree: newPartialTree }) this.lastCheckbox = ourItem.id! diff --git a/packages/@uppy/provider-views/src/utils/PartialTreeUtils.ts b/packages/@uppy/provider-views/src/utils/PartialTreeUtils.ts index ac81e582b4..9354bb53e3 100644 --- a/packages/@uppy/provider-views/src/utils/PartialTreeUtils.ts +++ b/packages/@uppy/provider-views/src/utils/PartialTreeUtils.ts @@ -1,7 +1,7 @@ import type { PartialTree, PartialTreeFile, PartialTreeFolder, PartialTreeFolderNode } from "@uppy/core/lib/Uppy" import type { CompanionFile } from "@uppy/utils/lib/CompanionFile" -const getPartialTreeAfterTogglingCheckboxes = ( +const afterToggleCheckbox = ( oldPartialTree: PartialTree, ourItem: PartialTreeFolderNode | PartialTreeFile, validateRestrictions: (file: CompanionFile) => object | null, @@ -84,7 +84,7 @@ const getPartialTreeAfterTogglingCheckboxes = ( return newPartialTree } -const clickOnFolder = ( +const afterClickOnFolder = ( oldPartialTree: PartialTree, currentItems: CompanionFile[], clickedFolder: PartialTreeFolder, @@ -135,7 +135,7 @@ const clickOnFolder = ( return newPartialTree } -const getPartialTreeAfterScroll = ( +const afterScroll = ( oldPartialTree: PartialTree, currentFolderId: string | null, items: CompanionFile[], @@ -181,4 +181,4 @@ const getPartialTreeAfterScroll = ( return newPartialTree } -export default { getPartialTreeAfterTogglingCheckboxes, clickOnFolder, getPartialTreeAfterScroll } +export default { afterToggleCheckbox, afterClickOnFolder, afterScroll } From 5200d3d63baea5d5a82da85c50903fa351ad3804 Mon Sep 17 00:00:00 2001 From: Evgenia Karunus Date: Thu, 11 Apr 2024 12:58:24 +0400 Subject: [PATCH 044/170] `.donePicking()` - implement using recursion --- .../src/utils/recursivelyFetch.ts | 115 ++++++++++++++++++ 1 file changed, 115 insertions(+) create mode 100644 packages/@uppy/provider-views/src/utils/recursivelyFetch.ts diff --git a/packages/@uppy/provider-views/src/utils/recursivelyFetch.ts b/packages/@uppy/provider-views/src/utils/recursivelyFetch.ts new file mode 100644 index 0000000000..d4e4518e4f --- /dev/null +++ b/packages/@uppy/provider-views/src/utils/recursivelyFetch.ts @@ -0,0 +1,115 @@ +import type { PartialTree, PartialTreeFile, PartialTreeFolder, PartialTreeFolderNode, PartialTreeId } from "@uppy/core/lib/Uppy" +import type { RequestOptions } from "@uppy/utils/lib/CompanionClientProvider" +import type { CompanionFile } from "@uppy/utils/lib/CompanionFile" +import PQueue from "p-queue" + +interface ApiProviderList { + (directory: string | null, options: RequestOptions): Promise<{ + username: string; + nextPagePath: string | null; + items: CompanionFile[]; + }> +} + +const getAbsPath = (partialTree: PartialTree, file: PartialTreeFile) : (PartialTreeFile | PartialTreeFolderNode)[] => { + const path : (PartialTreeFile | PartialTreeFolderNode)[] = [] + let parent: PartialTreeFile | PartialTreeFolder = file + while (true) { + if (parent.type === 'root') break + path.push(parent) + parent = partialTree.find((folder) => folder.id === (parent as PartialTreeFolderNode).parentId) as PartialTreeFolder + } + + return path.toReversed() +} + +const getRelPath = (absPath: (PartialTreeFile | PartialTreeFolderNode)[]) : (PartialTreeFile | PartialTreeFolderNode)[] => { + const firstCheckedFolderIndex = absPath.findIndex((i) => i.type === 'folder' && i.status === 'checked') + const relPath = absPath.slice(firstCheckedFolderIndex) + return relPath +} + + +const recursivelyFetch = async (queue: PQueue, fullTree: PartialTree, poorFolder: PartialTreeFolderNode, apiProviderList: ApiProviderList): Promise => { + let items : CompanionFile[] = [] + let currentPath : PartialTreeId = poorFolder.cached ? poorFolder.nextPagePath : poorFolder.id + while (currentPath) { + const response = await apiProviderList(currentPath, {}) + items = items.concat(response.items) + currentPath = response.nextPagePath + } + + let newFolders = items.filter((i) => i.isFolder === true) + let newFiles = items.filter((i) => i.isFolder === false) + + const folders : PartialTreeFolderNode[] = newFolders.map((folder) => ({ + type: 'folder', + id: folder.requestPath, + + cached: false, + nextPagePath: null, + + status: 'checked', + parentId: poorFolder.id, + data: folder, + })) + const files : PartialTreeFile[] = newFiles.map((file) => ({ + type: 'file', + id: file.requestPath, + + status: 'checked', + parentId: poorFolder.id, + data: file, + })) + + poorFolder.cached = true + poorFolder.nextPagePath = null + fullTree.push(...files, ...folders) + + folders.forEach(async (folder) => { + queue.add(async () => + await recursivelyFetch(queue, fullTree, folder, apiProviderList) + ) + }) + + return [] +} + +const donePickingReal = async (partialTree: PartialTree, apiProviderList: ApiProviderList) : Promise => { + const queue = new PQueue({ concurrency: 6 }) + + // fill up the missing parts of a partialTree! + let fullTree : PartialTree = JSON.parse(JSON.stringify(partialTree)) + const poorFolders = partialTree.filter((item) => + item.type === 'folder' && + item.status === 'checked' && + // either "not yet cached at all" or "some pages are left to fetch" + (item.cached === false || item.nextPagePath) + ) as PartialTreeFolderNode[] + // per each poor folder, recursively fetch all files and make them .checked!!! + poorFolders.forEach((poorFolder) => { + queue.add(async () => + await recursivelyFetch(queue, fullTree, poorFolder, apiProviderList) + ) + }) + + await queue.onIdle() + + // Return all 'checked' files + const checkedFiles = partialTree.filter((item) => item.type === 'file' && item.status === 'checked') as PartialTreeFile[] + + const uppyFiles = checkedFiles.map((file) => { + const absPath = getAbsPath(partialTree, file) + const relPath = getRelPath(absPath) + + return { + ...file.data, + absDirPath: absPath.join('/'), + relDirPath: relPath.join('/') + } + }) + + return uppyFiles +} + +export default donePickingReal; From d183ce48ffbd6bf47f44f4c900205b31f3a67e23 Mon Sep 17 00:00:00 2001 From: Evgenia Karunus Date: Thu, 11 Apr 2024 14:24:04 +0400 Subject: [PATCH 045/170] `.donePicking()` - integrate with `` --- .../src/ProviderView/ProviderView.tsx | 158 +----------------- ...recursivelyFetch.ts => fillPartialTree.ts} | 31 ++-- 2 files changed, 19 insertions(+), 170 deletions(-) rename packages/@uppy/provider-views/src/utils/{recursivelyFetch.ts => fillPartialTree.ts} (72%) diff --git a/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx b/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx index c6db41d405..a21a98c7e4 100644 --- a/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx +++ b/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx @@ -25,6 +25,7 @@ import View, { type ViewOptions } from '../View.ts' // @ts-ignore We don't want TS to generate types for the package.json import packageJson from '../../package.json' import PartialTreeUtils from '../utils/PartialTreeUtils.ts' +import fillPartialTree from '../utils/fillPartialTree.ts' function formatBreadcrumbs(breadcrumbs: PartialTreeFolder[]): string { const nonrootFoldes = breadcrumbs @@ -336,158 +337,13 @@ export default class ProviderView extends View< } } - async #recursivelyListAllFiles({ - requestPath, - absDirPath, - relDirPath, - queue, - onFiles, - signal, - }: { - requestPath: string - absDirPath: string - relDirPath: string - queue: PQueue - onFiles: (files: CompanionFile[]) => void - signal: AbortSignal - }) { - let curPath = requestPath - - while (curPath) { - const res = await this.#list({ requestPath: curPath, signal }) - curPath = res.nextPagePath - - const files = res.items.filter((item) => !item.isFolder) - const folders = res.items.filter((item) => item.isFolder) - - onFiles(files) - - // recursively queue call to self for each folder - const promises = folders.map(async (folder) => - queue.add(async () => - this.#recursivelyListAllFiles({ - requestPath: folder.requestPath, - absDirPath: prependPath(absDirPath, folder.name), - relDirPath: prependPath(relDirPath, folder.name), - queue, - onFiles, - signal, - }), - ), - ) - await Promise.all(promises) // in case we get an error - } - } - async donePicking(): Promise { - // this.setLoading(true) - // try { - // await this.#withAbort(async (signal) => { - // const { partialTree } = this.plugin.getPluginState() - // const currentSelection = partialTree.filter((item) => item.status === "checked") - - // const messages: string[] = [] - // const newFiles: CompanionFile[] = [] - - // for (const selectedItem of currentSelection) { - // const requestPath = selectedItem.id - - // const withRelDirPath = (newItem: CompanionFile) => ({ - // ...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 as string) - // .replace(selectedItem.data.absDirPath as string, '') - // .replace(/^\//, ''), - // }) - - // if (selectedItem.data.isFolder) { - // let isEmpty = true - // let numNewFiles = 0 - - // const queue = new PQueue({ concurrency: 6 }) - - // const onFiles = (files: CompanionFile[]) => { - // 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 - // } - // } - - // await this.#recursivelyListAllFiles({ - // requestPath, - // absDirPath: prependPath( - // selectedItem.data.absDirPath, - // selectedItem.data.name, - // ), - // relDirPath: selectedItem.data.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.data.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.data.name, - // }) - // } - - // messages.push(message) - // } else { - // newFiles.push(withRelDirPath(selectedItem.data)) - // } - // } - - // // 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 files from a remote provider') - // this.plugin.uppy.addFiles( - // // @ts-expect-error `addFiles` expects `body` to be `File` or `Blob`, - // // but as the todo comment in `View.ts` indicates, we strangly pass `CompanionFile` as `body`. - // // For now it's better to ignore than to have a potential breaking change. - // newFiles.map((file) => this.getTagFile(file, this.requestClientId)), - // ) - - // this.plugin.setPluginState({ filterInput: '' }) - // messages.forEach((message) => this.plugin.uppy.info(message)) - - // this.clearSelection() - // }) - // } catch (err) { - // this.handleError(err) - // } finally { - // this.setLoading(false) - // } + const { partialTree } = this.plugin.getPluginState() + this.setLoading(true) + const uppyFiles: CompanionFile[] = await fillPartialTree(partialTree, this.provider) + const filesToAdd = uppyFiles.map((file) => this.getTagFile(file)) + this.plugin.uppy.addFiles(filesToAdd) + this.setLoading(false) } getBreadcrumbs = () : PartialTreeFolder[] => { diff --git a/packages/@uppy/provider-views/src/utils/recursivelyFetch.ts b/packages/@uppy/provider-views/src/utils/fillPartialTree.ts similarity index 72% rename from packages/@uppy/provider-views/src/utils/recursivelyFetch.ts rename to packages/@uppy/provider-views/src/utils/fillPartialTree.ts index d4e4518e4f..4fa7b7826a 100644 --- a/packages/@uppy/provider-views/src/utils/recursivelyFetch.ts +++ b/packages/@uppy/provider-views/src/utils/fillPartialTree.ts @@ -1,15 +1,8 @@ import type { PartialTree, PartialTreeFile, PartialTreeFolder, PartialTreeFolderNode, PartialTreeId } from "@uppy/core/lib/Uppy" -import type { RequestOptions } from "@uppy/utils/lib/CompanionClientProvider" +import type { CompanionClientProvider, RequestOptions } from "@uppy/utils/lib/CompanionClientProvider" import type { CompanionFile } from "@uppy/utils/lib/CompanionFile" import PQueue from "p-queue" -interface ApiProviderList { - (directory: string | null, options: RequestOptions): Promise<{ - username: string; - nextPagePath: string | null; - items: CompanionFile[]; - }> -} const getAbsPath = (partialTree: PartialTree, file: PartialTreeFile) : (PartialTreeFile | PartialTreeFolderNode)[] => { const path : (PartialTreeFile | PartialTreeFolderNode)[] = [] @@ -29,12 +22,12 @@ const getRelPath = (absPath: (PartialTreeFile | PartialTreeFolderNode)[]) : (Par return relPath } - -const recursivelyFetch = async (queue: PQueue, fullTree: PartialTree, poorFolder: PartialTreeFolderNode, apiProviderList: ApiProviderList): Promise => { +const recursivelyFetch = async (queue: PQueue, poorTree: PartialTree, poorFolder: PartialTreeFolderNode, provider: CompanionClientProvider): Promise => { let items : CompanionFile[] = [] let currentPath : PartialTreeId = poorFolder.cached ? poorFolder.nextPagePath : poorFolder.id while (currentPath) { - const response = await apiProviderList(currentPath, {}) + const response = await provider.list(currentPath, {}) + console.log({ currentPath, response }); items = items.concat(response.items) currentPath = response.nextPagePath } @@ -64,22 +57,22 @@ const recursivelyFetch = async (queue: PQueue, fullTree: PartialTree, poorFolder poorFolder.cached = true poorFolder.nextPagePath = null - fullTree.push(...files, ...folders) + poorTree.push(...files, ...folders) folders.forEach(async (folder) => { queue.add(async () => - await recursivelyFetch(queue, fullTree, folder, apiProviderList) + await recursivelyFetch(queue, poorTree, folder, provider) ) }) return [] } -const donePickingReal = async (partialTree: PartialTree, apiProviderList: ApiProviderList) : Promise => { +const fillPartialTree = async (partialTree: PartialTree, provider: CompanionClientProvider) : Promise => { const queue = new PQueue({ concurrency: 6 }) // fill up the missing parts of a partialTree! - let fullTree : PartialTree = JSON.parse(JSON.stringify(partialTree)) + let poorTree : PartialTree = JSON.parse(JSON.stringify(partialTree)) const poorFolders = partialTree.filter((item) => item.type === 'folder' && item.status === 'checked' && @@ -89,17 +82,17 @@ const donePickingReal = async (partialTree: PartialTree, apiProviderList: ApiPro // per each poor folder, recursively fetch all files and make them .checked!!! poorFolders.forEach((poorFolder) => { queue.add(async () => - await recursivelyFetch(queue, fullTree, poorFolder, apiProviderList) + await recursivelyFetch(queue, poorTree, poorFolder, provider) ) }) await queue.onIdle() // Return all 'checked' files - const checkedFiles = partialTree.filter((item) => item.type === 'file' && item.status === 'checked') as PartialTreeFile[] + const checkedFiles = poorTree.filter((item) => item.type === 'file' && item.status === 'checked') as PartialTreeFile[] const uppyFiles = checkedFiles.map((file) => { - const absPath = getAbsPath(partialTree, file) + const absPath = getAbsPath(poorTree, file) const relPath = getRelPath(absPath) return { @@ -112,4 +105,4 @@ const donePickingReal = async (partialTree: PartialTree, apiProviderList: ApiPro return uppyFiles } -export default donePickingReal; +export default fillPartialTree; From 0cdad8b6061c1f763927c7687f027ff9ea83b465 Mon Sep 17 00:00:00 2001 From: Evgenia Karunus Date: Fri, 12 Apr 2024 15:39:50 +0400 Subject: [PATCH 046/170] `donePicking()` - show notifications after addition --- .../src/ProviderView/ProviderView.tsx | 34 +++++++++++++++++-- .../src/utils/fillPartialTree.ts | 2 +- 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx b/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx index a21a98c7e4..448d36ddb9 100644 --- a/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx +++ b/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx @@ -11,7 +11,7 @@ import type { PartialTreeFolderNode, PartialTreeFile, } from '@uppy/core/lib/Uppy.ts' -import type { Body, Meta, UppyFile } from '@uppy/utils/lib/UppyFile' +import type { Body, Meta, TagFile, UppyFile } from '@uppy/utils/lib/UppyFile' import type { CompanionFile } from '@uppy/utils/lib/CompanionFile.ts' import type Translator from '@uppy/utils/lib/Translator' import type { DefinePluginOpts } from '@uppy/core/lib/BasePlugin.ts' @@ -341,7 +341,37 @@ export default class ProviderView extends View< const { partialTree } = this.plugin.getPluginState() this.setLoading(true) const uppyFiles: CompanionFile[] = await fillPartialTree(partialTree, this.provider) - const filesToAdd = uppyFiles.map((file) => this.getTagFile(file)) + const filesToAdd : TagFile[] = [] + const filesAlreadyAdded : TagFile[] = [] + const filesNotPassingRestrictions : TagFile[] = [] + + uppyFiles.forEach((uppyFile) => { + const tagFile = this.getTagFile(uppyFile) + + if (this.validateRestrictions(uppyFile)) { + filesNotPassingRestrictions.push(tagFile) + return + } + + const id = getSafeFileId(tagFile) + if (this.plugin.uppy.checkIfFileAlreadyExists(id)) { + filesAlreadyAdded.push(tagFile) + return + } + + filesToAdd.push(tagFile) + }) + + if (filesToAdd.length > 0) { + this.plugin.uppy.info(`${filesToAdd.length} files added`) + } + if (filesAlreadyAdded.length > 0) { + this.plugin.uppy.info(`Not adding ${filesAlreadyAdded.length} files because they already exist`) + } + if (filesNotPassingRestrictions.length > 0) { + this.plugin.uppy.info(`Not adding ${filesNotPassingRestrictions.length} files they didn't pass restrictions`) + } + this.plugin.uppy.addFiles(filesToAdd) this.setLoading(false) } diff --git a/packages/@uppy/provider-views/src/utils/fillPartialTree.ts b/packages/@uppy/provider-views/src/utils/fillPartialTree.ts index 4fa7b7826a..450d05b80e 100644 --- a/packages/@uppy/provider-views/src/utils/fillPartialTree.ts +++ b/packages/@uppy/provider-views/src/utils/fillPartialTree.ts @@ -73,7 +73,7 @@ const fillPartialTree = async (partialTree: PartialTree, provider: CompanionClie // fill up the missing parts of a partialTree! let poorTree : PartialTree = JSON.parse(JSON.stringify(partialTree)) - const poorFolders = partialTree.filter((item) => + const poorFolders = poorTree.filter((item) => item.type === 'folder' && item.status === 'checked' && // either "not yet cached at all" or "some pages are left to fetch" From 216e05a9339fb9f7c755a741c17c81eba10901b5 Mon Sep 17 00:00:00 2001 From: Evgenia Karunus Date: Fri, 12 Apr 2024 16:24:04 +0400 Subject: [PATCH 047/170] `#list()` - get rid of unnecessary indirection --- .../src/ProviderView/ProviderView.tsx | 49 +++---------------- 1 file changed, 6 insertions(+), 43 deletions(-) diff --git a/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx b/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx index 448d36ddb9..b0a81b930f 100644 --- a/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx +++ b/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx @@ -1,17 +1,14 @@ import { h } from 'preact' -import PQueue from 'p-queue' import { getSafeFileId } from '@uppy/utils/lib/generateFileID' import type { UnknownProviderPlugin, - Uppy, - PartialTree, PartialTreeFolder, PartialTreeFolderNode, PartialTreeFile, } from '@uppy/core/lib/Uppy.ts' -import type { Body, Meta, TagFile, UppyFile } from '@uppy/utils/lib/UppyFile' +import type { Body, Meta, TagFile } from '@uppy/utils/lib/UppyFile' import type { CompanionFile } from '@uppy/utils/lib/CompanionFile.ts' import type Translator from '@uppy/utils/lib/Translator' import type { DefinePluginOpts } from '@uppy/core/lib/BasePlugin.ts' @@ -27,19 +24,6 @@ import packageJson from '../../package.json' import PartialTreeUtils from '../utils/PartialTreeUtils.ts' import fillPartialTree from '../utils/fillPartialTree.ts' -function formatBreadcrumbs(breadcrumbs: PartialTreeFolder[]): string { - const nonrootFoldes = breadcrumbs - .filter((folder) => folder.type === 'folder') as PartialTreeFolderNode[] - return nonrootFoldes - .map((folder) => folder.data.name) - .join('/') -} - -function prependPath(path: string | undefined, component: string): string { - if (!path) return component - return `${path}/${component}` -} - export function defaultPickerIcon(): JSX.Element { return ( extends View< } } - async #list({ requestPath, signal }: { requestPath: string | null, signal: AbortSignal }) { - const { username, nextPagePath, items } = await this.provider.list(requestPath, { signal }) - this.username = username || this.username - - return { items, nextPagePath } - } - /** * 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?) @@ -194,10 +171,10 @@ export default class ProviderView extends View< let currentPagePath = folderId let currentItems: CompanionFile[] = [] do { - const { items, nextPagePath } = await this.#list({ - requestPath: currentPagePath, - signal - }) + const { username, nextPagePath, items } = await this.provider.list(currentPagePath, { signal }) + // It's important to set the username during one of our first fetches + this.username = username + currentPagePath = nextPagePath currentItems = currentItems.concat(items) this.setLoading(this.plugin.uppy.i18n('loadedXFiles', { numFiles: items.length })) @@ -212,16 +189,6 @@ export default class ProviderView extends View< }) }) - - - - - - - - - - } catch (err) { // This is the first call that happens when the provider view loads, after auth, so it's probably nice to show any // error occurring here to the user. @@ -320,11 +287,7 @@ export default class ProviderView extends View< try { await this.#withAbort(async (signal) => { - - const { items, nextPagePath } = await this.#list({ - requestPath: currentFolder.nextPagePath!, - signal - }) + const { nextPagePath, items } = await this.provider.list(currentFolder.nextPagePath!, { signal }) const newPartialTree = PartialTreeUtils.afterScroll(partialTree, currentFolderId, items, nextPagePath, this.validateRestrictions) this.plugin.setPluginState({ partialTree: newPartialTree }) From 4c8c3365af31998233fc3bd2fdb9cf75b41854ac Mon Sep 17 00:00:00 2001 From: Evgenia Karunus Date: Mon, 15 Apr 2024 15:41:36 +0400 Subject: [PATCH 048/170] ProviderView.tsx - add `signal` everywhere, reduce try/catch indents everywhere --- .../src/ProviderView/ProviderView.tsx | 211 ++++++++---------- .../src/utils/fillPartialTree.ts | 10 +- 2 files changed, 103 insertions(+), 118 deletions(-) diff --git a/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx b/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx index b0a81b930f..520b73124a 100644 --- a/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx +++ b/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx @@ -164,32 +164,28 @@ export default class ProviderView extends View< return } - try { - this.setLoading(true) - await this.#withAbort(async (signal) => { - - let currentPagePath = folderId - let currentItems: CompanionFile[] = [] - do { - const { username, nextPagePath, items } = await this.provider.list(currentPagePath, { signal }) - // It's important to set the username during one of our first fetches - this.username = username - - currentPagePath = nextPagePath - currentItems = currentItems.concat(items) - this.setLoading(this.plugin.uppy.i18n('loadedXFiles', { numFiles: items.length })) - } while (this.opts.loadAllFiles && currentPagePath) - - const newPartialTree = PartialTreeUtils.afterClickOnFolder(partialTree, currentItems, clickedFolder, this.validateRestrictions, currentPagePath) - - this.plugin.setPluginState({ - partialTree: newPartialTree, - currentFolderId: folderId, - filterInput: '' - }) + this.setLoading(true) + await this.#withAbort(async (signal) => { + let currentPagePath = folderId + let currentItems: CompanionFile[] = [] + do { + const { username, nextPagePath, items } = await this.provider.list(currentPagePath, { signal }) + // It's important to set the username during one of our first fetches + this.username = username + + currentPagePath = nextPagePath + currentItems = currentItems.concat(items) + this.setLoading(this.plugin.uppy.i18n('loadedXFiles', { numFiles: items.length })) + } while (this.opts.loadAllFiles && currentPagePath) + + const newPartialTree = PartialTreeUtils.afterClickOnFolder(partialTree, currentItems, clickedFolder, this.validateRestrictions, currentPagePath) + + this.plugin.setPluginState({ + partialTree: newPartialTree, + currentFolderId: folderId, + filterInput: '' }) - - } catch (err) { + }).catch((err) => { // This is the first call that happens when the provider view loads, after auth, so it's probably nice to show any // error occurring here to the user. if (err?.name === 'UserFacingApiError') { @@ -200,48 +196,43 @@ export default class ProviderView extends View< ) return } - this.handleError(err) - } finally { - this.setLoading(false) - } + }) + + this.setLoading(false) } /** * Removes session token on client side. */ async logout(): Promise { - try { - await this.#withAbort(async (signal) => { - const res = await this.provider.logout<{ - ok: boolean - revoked: boolean - manual_revoke_url: string - }>({ - signal, - }) - // res.ok is from the JSON body, not to be confused with Response.ok - 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, - currentFolderId: null, - partialTree: [], - filterInput: '', - } - this.plugin.setPluginState(newState) - } + await this.#withAbort(async (signal) => { + const res = await this.provider.logout<{ + ok: boolean + revoked: boolean + manual_revoke_url: string + }>({ + signal, }) - } catch (err) { - this.handleError(err) - } + // res.ok is from the JSON body, not to be confused with Response.ok + 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, + currentFolderId: null, + partialTree: [], + filterInput: '', + } + this.plugin.setPluginState(newState) + } + }).catch(this.handleError) } filterQuery(input: string): void { @@ -253,17 +244,15 @@ export default class ProviderView extends View< } async handleAuth(authFormData?: unknown): Promise { - try { - await this.#withAbort(async (signal) => { - this.setLoading(true) - await this.provider.login({ authFormData, signal }) - this.plugin.setPluginState({ authenticated: true }) - await Promise.all([ - this.provider.fetchPreAuthToken(), - this.getFolder(this.plugin.rootFolderId), - ]) - }) - } catch (err) { + await this.#withAbort(async (signal) => { + this.setLoading(true) + await this.provider.login({ authFormData, signal }) + this.plugin.setPluginState({ authenticated: true }) + await Promise.all([ + this.provider.fetchPreAuthToken(), + this.getFolder(this.plugin.rootFolderId), + ]) + }).catch((err) => { if (err.name === 'UserFacingApiError') { this.plugin.uppy.info( { message: this.plugin.uppy.i18n(err.message) }, @@ -274,9 +263,8 @@ export default class ProviderView extends View< } this.plugin.uppy.log(`login failed: ${err.message}`) - } finally { - this.setLoading(false) - } + }) + this.setLoading(false) } async handleScroll(event: Event): Promise { @@ -284,58 +272,55 @@ export default class ProviderView extends View< const currentFolder = partialTree.find((i) => i.id === currentFolderId) as PartialTreeFolder if (this.shouldHandleScroll(event) && currentFolder.nextPagePath) { this.isHandlingScroll = true + await this.#withAbort(async (signal) => { + const { nextPagePath, items } = await this.provider.list(currentFolder.nextPagePath!, { signal }) + const newPartialTree = PartialTreeUtils.afterScroll(partialTree, currentFolderId, items, nextPagePath, this.validateRestrictions) - try { - await this.#withAbort(async (signal) => { - const { nextPagePath, items } = await this.provider.list(currentFolder.nextPagePath!, { signal }) - const newPartialTree = PartialTreeUtils.afterScroll(partialTree, currentFolderId, items, nextPagePath, this.validateRestrictions) - - this.plugin.setPluginState({ partialTree: newPartialTree }) - }) - } catch (error) { - this.handleError(error) - } finally { - this.isHandlingScroll = false - } + this.plugin.setPluginState({ partialTree: newPartialTree }) + }).catch(this.handleError) + this.isHandlingScroll = false } } async donePicking(): Promise { const { partialTree } = this.plugin.getPluginState() this.setLoading(true) - const uppyFiles: CompanionFile[] = await fillPartialTree(partialTree, this.provider) - const filesToAdd : TagFile[] = [] - const filesAlreadyAdded : TagFile[] = [] - const filesNotPassingRestrictions : TagFile[] = [] - uppyFiles.forEach((uppyFile) => { - const tagFile = this.getTagFile(uppyFile) + await this.#withAbort(async (signal) => { + const uppyFiles: CompanionFile[] = await fillPartialTree(partialTree, this.provider, signal) + + const filesToAdd : TagFile[] = [] + const filesAlreadyAdded : TagFile[] = [] + const filesNotPassingRestrictions : TagFile[] = [] + + uppyFiles.forEach((uppyFile) => { + const tagFile = this.getTagFile(uppyFile) + + if (this.validateRestrictions(uppyFile)) { + filesNotPassingRestrictions.push(tagFile) + return + } + + const id = getSafeFileId(tagFile) + if (this.plugin.uppy.checkIfFileAlreadyExists(id)) { + filesAlreadyAdded.push(tagFile) + return + } + filesToAdd.push(tagFile) + }) - if (this.validateRestrictions(uppyFile)) { - filesNotPassingRestrictions.push(tagFile) - return + if (filesToAdd.length > 0) { + this.plugin.uppy.info(`${filesToAdd.length} files added`) } - - const id = getSafeFileId(tagFile) - if (this.plugin.uppy.checkIfFileAlreadyExists(id)) { - filesAlreadyAdded.push(tagFile) - return + if (filesAlreadyAdded.length > 0) { + this.plugin.uppy.info(`Not adding ${filesAlreadyAdded.length} files because they already exist`) } + if (filesNotPassingRestrictions.length > 0) { + this.plugin.uppy.info(`Not adding ${filesNotPassingRestrictions.length} files they didn't pass restrictions`) + } + this.plugin.uppy.addFiles(filesToAdd) + }).catch((err) => this.handleError(err)) - filesToAdd.push(tagFile) - }) - - if (filesToAdd.length > 0) { - this.plugin.uppy.info(`${filesToAdd.length} files added`) - } - if (filesAlreadyAdded.length > 0) { - this.plugin.uppy.info(`Not adding ${filesAlreadyAdded.length} files because they already exist`) - } - if (filesNotPassingRestrictions.length > 0) { - this.plugin.uppy.info(`Not adding ${filesNotPassingRestrictions.length} files they didn't pass restrictions`) - } - - this.plugin.uppy.addFiles(filesToAdd) this.setLoading(false) } diff --git a/packages/@uppy/provider-views/src/utils/fillPartialTree.ts b/packages/@uppy/provider-views/src/utils/fillPartialTree.ts index 450d05b80e..7d9dacc3b3 100644 --- a/packages/@uppy/provider-views/src/utils/fillPartialTree.ts +++ b/packages/@uppy/provider-views/src/utils/fillPartialTree.ts @@ -22,11 +22,11 @@ const getRelPath = (absPath: (PartialTreeFile | PartialTreeFolderNode)[]) : (Par return relPath } -const recursivelyFetch = async (queue: PQueue, poorTree: PartialTree, poorFolder: PartialTreeFolderNode, provider: CompanionClientProvider): Promise => { +const recursivelyFetch = async (queue: PQueue, poorTree: PartialTree, poorFolder: PartialTreeFolderNode, provider: CompanionClientProvider, signal: AbortSignal): Promise => { let items : CompanionFile[] = [] let currentPath : PartialTreeId = poorFolder.cached ? poorFolder.nextPagePath : poorFolder.id while (currentPath) { - const response = await provider.list(currentPath, {}) + const response = await provider.list(currentPath, { signal }) console.log({ currentPath, response }); items = items.concat(response.items) currentPath = response.nextPagePath @@ -61,14 +61,14 @@ const recursivelyFetch = async (queue: PQueue, poorTree: PartialTree, poorFolder folders.forEach(async (folder) => { queue.add(async () => - await recursivelyFetch(queue, poorTree, folder, provider) + await recursivelyFetch(queue, poorTree, folder, provider, signal) ) }) return [] } -const fillPartialTree = async (partialTree: PartialTree, provider: CompanionClientProvider) : Promise => { +const fillPartialTree = async (partialTree: PartialTree, provider: CompanionClientProvider, signal: AbortSignal) : Promise => { const queue = new PQueue({ concurrency: 6 }) // fill up the missing parts of a partialTree! @@ -82,7 +82,7 @@ const fillPartialTree = async (partialTree: PartialTree, provider: CompanionClie // per each poor folder, recursively fetch all files and make them .checked!!! poorFolders.forEach((poorFolder) => { queue.add(async () => - await recursivelyFetch(queue, poorTree, poorFolder, provider) + await recursivelyFetch(queue, poorTree, poorFolder, provider, signal) ) }) From c35cd4c6e1c73df4246470520c48f41a398632b3 Mon Sep 17 00:00:00 2001 From: Evgenia Karunus Date: Tue, 16 Apr 2024 16:34:07 +0400 Subject: [PATCH 049/170] `handleError()` - make error handling uniform --- .../src/ProviderView/ProviderView.tsx | 29 ++----------------- packages/@uppy/provider-views/src/View.ts | 26 ++++++++++------- 2 files changed, 18 insertions(+), 37 deletions(-) diff --git a/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx b/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx index 520b73124a..5184252c51 100644 --- a/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx +++ b/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx @@ -185,19 +185,7 @@ export default class ProviderView extends View< currentFolderId: folderId, filterInput: '' }) - }).catch((err) => { - // This is the first call that happens when the provider view loads, after auth, so it's probably nice to show any - // error occurring here to the user. - if (err?.name === 'UserFacingApiError') { - this.plugin.uppy.info( - { message: this.plugin.uppy.i18n(err.message) }, - 'warning', - 5000, - ) - return - } - this.handleError(err) - }) + }).catch(this.handleError) this.setLoading(false) } @@ -252,18 +240,7 @@ export default class ProviderView extends View< this.provider.fetchPreAuthToken(), this.getFolder(this.plugin.rootFolderId), ]) - }).catch((err) => { - if (err.name === 'UserFacingApiError') { - this.plugin.uppy.info( - { message: this.plugin.uppy.i18n(err.message) }, - 'warning', - 5000, - ) - return - } - - this.plugin.uppy.log(`login failed: ${err.message}`) - }) + }).catch(this.handleError) this.setLoading(false) } @@ -319,7 +296,7 @@ export default class ProviderView extends View< this.plugin.uppy.info(`Not adding ${filesNotPassingRestrictions.length} files they didn't pass restrictions`) } this.plugin.uppy.addFiles(filesToAdd) - }).catch((err) => this.handleError(err)) + }).catch(this.handleError) this.setLoading(false) } diff --git a/packages/@uppy/provider-views/src/View.ts b/packages/@uppy/provider-views/src/View.ts index fadafbd08e..b478d92249 100644 --- a/packages/@uppy/provider-views/src/View.ts +++ b/packages/@uppy/provider-views/src/View.ts @@ -137,20 +137,24 @@ export default class View< handleError(error: Error): void { const { uppy } = this.plugin - const message = uppy.i18n('companionError') - - uppy.log(error.toString()) - - if ( - (error as any).isAuthError || - (error.cause as Error)?.name === 'AbortError' - ) { - // authError just means we're not authenticated, don't show to user - // AbortError means the user has clicked "cancel" on an operation + // authError just means we're not authenticated, don't report it + if ((error as any).isAuthError) { + return + } + // AbortError means the user has clicked "cancel" on an operation + if (error.name === 'AbortError') { + uppy.log('Aborting request', 'warning') return } + uppy.log(error, 'error') + + if (error.name === 'UserFacingApiError') { + uppy.info({ + message: uppy.i18n('companionError'), + details: uppy.i18n(error.message) + }, 'warning', 5000) + } - uppy.info({ message, details: error.toString() }, 'error', 5000) } registerRequestClient(): void { From fd6910207b53e952e9ba710119d300fbf25ffe62 Mon Sep 17 00:00:00 2001 From: Evgenia Karunus Date: Tue, 16 Apr 2024 17:00:29 +0400 Subject: [PATCH 050/170] `state.isSearchVisible` - remove, it's just not used anywhere --- packages/@uppy/core/src/Uppy.ts | 1 - packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx | 1 - 2 files changed, 2 deletions(-) diff --git a/packages/@uppy/core/src/Uppy.ts b/packages/@uppy/core/src/Uppy.ts index 75230ba3a8..6dab9740ee 100644 --- a/packages/@uppy/core/src/Uppy.ts +++ b/packages/@uppy/core/src/Uppy.ts @@ -112,7 +112,6 @@ export type UnknownProviderPluginState = { didFirstRender: boolean filterInput: string loading: boolean | string - isSearchVisible: boolean partialTree: PartialTree currentFolderId: string | null } diff --git a/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx b/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx index 5184252c51..1846dd4b3e 100644 --- a/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx +++ b/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx @@ -107,7 +107,6 @@ export default class ProviderView extends View< ], currentFolderId: null, filterInput: '', - isSearchVisible: false, }) this.registerRequestClient() From 1bef2b4866b4698048970b00442379bcdcf8e445 Mon Sep 17 00:00:00 2001 From: Evgenia Karunus Date: Tue, 16 Apr 2024 17:18:47 +0400 Subject: [PATCH 051/170] state - reuse default state --- .../src/ProviderView/ProviderView.tsx | 40 +++++++++---------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx b/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx index 1846dd4b3e..5d65d9cd8f 100644 --- a/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx +++ b/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx @@ -7,6 +7,7 @@ import type { PartialTreeFolder, PartialTreeFolderNode, PartialTreeFile, + UnknownProviderPluginState, } from '@uppy/core/lib/Uppy.ts' import type { Body, Meta, TagFile } from '@uppy/utils/lib/UppyFile' import type { CompanionFile } from '@uppy/utils/lib/CompanionFile.ts' @@ -63,6 +64,20 @@ type Opts = DefinePluginOpts< keyof typeof defaultOptions > +const getDefaultState = (rootFolderId: string | null) : Partial => ({ + authenticated: undefined, // we don't know yet + partialTree: [ + { + type: 'root', + id: rootFolderId, + cached: false, + nextPagePath: null + } + ], + currentFolderId: null, + filterInput: '' +}) + /** * Class to easily generate generic views for Provider plugins */ @@ -95,19 +110,7 @@ export default class ProviderView extends View< this.render = this.render.bind(this) // Set default state for the plugin - this.plugin.setPluginState({ - authenticated: undefined, // we don't know yet - partialTree: [ - { - type: 'root', - id: this.plugin.rootFolderId, - cached: false, - nextPagePath: null - } - ], - currentFolderId: null, - filterInput: '', - }) + this.plugin.setPluginState(getDefaultState(this.plugin.rootFolderId)) this.registerRequestClient() } @@ -211,13 +214,10 @@ export default class ProviderView extends View< this.plugin.uppy.info(message, 'info', 7000) } - const newState = { - authenticated: false, - currentFolderId: null, - partialTree: [], - filterInput: '', - } - this.plugin.setPluginState(newState) + this.plugin.setPluginState({ + ...getDefaultState(this.plugin.rootFolderId), + authenticated: false + }) } }).catch(this.handleError) } From dc339f4a63538d640a3877fe102f56e9bc73911f Mon Sep 17 00:00:00 2001 From: Evgenia Karunus Date: Tue, 16 Apr 2024 17:42:54 +0400 Subject: [PATCH 052/170] state - reset state on close panel (like we discussed in the uppy call) --- .../provider-views/src/ProviderView/ProviderView.tsx | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx b/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx index 5d65d9cd8f..4ba21355cf 100644 --- a/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx +++ b/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx @@ -75,7 +75,8 @@ const getDefaultState = (rootFolderId: string | null) : Partial extends View< // Set default state for the plugin this.plugin.setPluginState(getDefaultState(this.plugin.rootFolderId)) + const onClosePanel = () => { + this.plugin.setPluginState(getDefaultState(this.plugin.rootFolderId)) + } + // @ts-expect-error this should be typed in @uppy/dashboard. + this.plugin.uppy.on('dashboard:close-panel', onClosePanel) + this.plugin.uppy.on('cancel-all', onClosePanel) + this.registerRequestClient() } - // eslint-disable-next-line class-methods-use-this tearDown(): void { // Nothing. } From 37e9f3374833f83af3214d76dff233127259e186 Mon Sep 17 00:00:00 2001 From: Evgenia Karunus Date: Tue, 16 Apr 2024 17:59:37 +0400 Subject: [PATCH 053/170] methods - remove unnecessary indirection in state setting --- .../src/ProviderView/ProviderView.tsx | 17 ++--------------- 1 file changed, 2 insertions(+), 15 deletions(-) diff --git a/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx b/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx index 4ba21355cf..d537d6bf5c 100644 --- a/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx +++ b/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx @@ -98,16 +98,11 @@ export default class ProviderView extends View< ) { super(plugin, { ...defaultOptions, ...opts }) - // Logic - this.filterQuery = this.filterQuery.bind(this) - this.clearFilter = this.clearFilter.bind(this) this.getFolder = this.getFolder.bind(this) this.logout = this.logout.bind(this) this.handleAuth = this.handleAuth.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 @@ -229,14 +224,6 @@ export default class ProviderView extends View< }).catch(this.handleError) } - filterQuery(input: string): void { - this.plugin.setPluginState({ filterInput: input }) - } - - clearFilter(): void { - this.plugin.setPluginState({ filterInput: '' }) - } - async handleAuth(authFormData?: unknown): Promise { await this.#withAbort(async (signal) => { this.setLoading(true) @@ -364,8 +351,8 @@ export default class ProviderView extends View< // For SearchFilterInput component showSearchFilter: targetViewOptions.showFilter, - search: this.filterQuery, - clearSearch: this.clearFilter, + search: (input: string | undefined) => this.plugin.setPluginState({ filterInput: input }), + clearSearch: () => this.plugin.setPluginState({ filterInput: '' }), searchTerm: filterInput, searchOnInput: true, searchInputLabel: i18n('filter'), From 2c869d6b17734e08bce25548a8d5656b289575bc Mon Sep 17 00:00:00 2001 From: Evgenia Karunus Date: Tue, 16 Apr 2024 18:35:50 +0400 Subject: [PATCH 054/170] `` - remove CloseWrappers, this is unnecessary indirection now too --- .../src/ProviderView/ProviderView.tsx | 28 +++++++------------ packages/@uppy/provider-views/src/View.ts | 14 ---------- 2 files changed, 10 insertions(+), 32 deletions(-) diff --git a/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx b/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx index d537d6bf5c..308ba3d064 100644 --- a/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx +++ b/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx @@ -16,7 +16,6 @@ import type { DefinePluginOpts } from '@uppy/core/lib/BasePlugin.ts' import AuthView from './AuthView.tsx' import Header from './Header.tsx' import Browser from '../Browser.tsx' -import CloseWrapper from '../CloseWrapper.ts' import View, { type ViewOptions } from '../View.ts' // eslint-disable-next-line @typescript-eslint/ban-ts-comment @@ -131,7 +130,6 @@ export default class ProviderView extends View< this.#abortController = abortController const cancelRequest = () => { abortController.abort() - this.clearSelection() } try { // @ts-expect-error this should be typed in @uppy/dashboard. @@ -379,24 +377,18 @@ export default class ProviderView extends View< if (authenticated === false) { return ( - - - + ) } - return ( - - {/* eslint-disable-next-line react/jsx-props-no-spreading */} - {...browserProps} /> - - ) + // eslint-disable-next-line react/jsx-props-no-spreading + return {...browserProps} /> } } diff --git a/packages/@uppy/provider-views/src/View.ts b/packages/@uppy/provider-views/src/View.ts index b478d92249..bea4b9b971 100644 --- a/packages/@uppy/provider-views/src/View.ts +++ b/packages/@uppy/provider-views/src/View.ts @@ -1,7 +1,6 @@ import type { PartialTree, PartialTreeFile, - PartialTreeFolder, PartialTreeFolderNode, UnknownProviderPlugin, UnknownSearchProviderPlugin, @@ -70,7 +69,6 @@ export default class View< this.isHandlingScroll = false this.handleError = this.handleError.bind(this) - this.clearSelection = this.clearSelection.bind(this) this.cancelPicking = this.cancelPicking.bind(this) this.validateRestrictions = this.validateRestrictions.bind(this) this.getNOfSelectedFiles = this.getNOfSelectedFiles.bind(this) @@ -114,18 +112,7 @@ export default class View< return scrollPosition < 50 && !this.isHandlingScroll } - clearSelection(): void { - const { partialTree } = this.plugin.getPluginState() - const newPartialTree : PartialTree = partialTree.map((item) => ({ - ...item, - status: "unchecked" - })) - this.plugin.setPluginState({ partialTree: newPartialTree, filterInput: '' }) - } - cancelPicking(): void { - this.clearSelection() - const dashboard = this.plugin.uppy.getPlugin('Dashboard') if (dashboard) { @@ -154,7 +141,6 @@ export default class View< details: uppy.i18n(error.message) }, 'warning', 5000) } - } registerRequestClient(): void { From 9550ec2a8d76b35f350ae41d3eb10b4b0625ce2a Mon Sep 17 00:00:00 2001 From: Evgenia Karunus Date: Tue, 16 Apr 2024 18:39:42 +0400 Subject: [PATCH 055/170] `this.requestClientId` - remove, again - this was unnecessary indirection --- packages/@uppy/provider-views/src/View.ts | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/packages/@uppy/provider-views/src/View.ts b/packages/@uppy/provider-views/src/View.ts index bea4b9b971..fc14966df2 100644 --- a/packages/@uppy/provider-views/src/View.ts +++ b/packages/@uppy/provider-views/src/View.ts @@ -53,8 +53,6 @@ export default class View< isHandlingScroll: boolean - requestClientId: string - isShiftKeyPressed: boolean lastCheckbox: string | undefined @@ -144,8 +142,7 @@ export default class View< } registerRequestClient(): void { - this.requestClientId = this.provider.provider - this.plugin.uppy.registerRequestClient(this.requestClientId, this.provider) + this.plugin.uppy.registerRequestClient(this.provider.provider, this.provider) } // TODO: document what is a "tagFile" or get rid of this concept @@ -171,7 +168,7 @@ export default class View< }, providerName: this.provider.name, provider: this.provider.provider, - requestClientId: this.requestClientId, + requestClientId: this.provider.provider, }, } From c8f80660dc5bb931dbdfacc29d39382420bf1931 Mon Sep 17 00:00:00 2001 From: Evgenia Karunus Date: Tue, 16 Apr 2024 19:21:32 +0400 Subject: [PATCH 056/170] `getTagFile()` - factor out into a separate file --- .../src/ProviderView/ProviderView.tsx | 5 +- .../SearchProviderView/SearchProviderView.tsx | 7 ++- packages/@uppy/provider-views/src/View.ts | 56 ----------------- .../provider-views/src/utils/getTagFile.ts | 60 +++++++++++++++++++ 4 files changed, 68 insertions(+), 60 deletions(-) create mode 100644 packages/@uppy/provider-views/src/utils/getTagFile.ts diff --git a/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx b/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx index 308ba3d064..b8aef0dd63 100644 --- a/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx +++ b/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx @@ -23,6 +23,7 @@ import View, { type ViewOptions } from '../View.ts' import packageJson from '../../package.json' import PartialTreeUtils from '../utils/PartialTreeUtils.ts' import fillPartialTree from '../utils/fillPartialTree.ts' +import getTagFile from '../utils/getTagFile.ts' export function defaultPickerIcon(): JSX.Element { return ( @@ -262,8 +263,8 @@ export default class ProviderView extends View< const filesNotPassingRestrictions : TagFile[] = [] uppyFiles.forEach((uppyFile) => { - const tagFile = this.getTagFile(uppyFile) - + const tagFile = getTagFile(uppyFile, this.plugin.id, this.provider, this.plugin.opts.companionUrl) + if (this.validateRestrictions(uppyFile)) { filesNotPassingRestrictions.push(tagFile) return diff --git a/packages/@uppy/provider-views/src/SearchProviderView/SearchProviderView.tsx b/packages/@uppy/provider-views/src/SearchProviderView/SearchProviderView.tsx index b2b4a1a842..bb1a2d3b8d 100644 --- a/packages/@uppy/provider-views/src/SearchProviderView/SearchProviderView.tsx +++ b/packages/@uppy/provider-views/src/SearchProviderView/SearchProviderView.tsx @@ -13,6 +13,7 @@ import View, { type ViewOptions } from '../View.ts' // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore We don't want TS to generate types for the package.json import packageJson from '../../package.json' +import getTagFile from '../utils/getTagFile.ts' const defaultState : Partial = { isInputMode: true, @@ -159,9 +160,11 @@ export default class SearchProviderView< const { partialTree } = this.plugin.getPluginState() this.plugin.uppy.log('Adding remote search provider files') const files = partialTree.filter((i) => i.type !== 'root' && i.status === 'checked') as PartialTreeFile[] - this.plugin.uppy.addFiles( - files.map((file) => this.getTagFile(file.data)), + const tagFiles = files.map((file) => + getTagFile(file.data, this.plugin.id, this.provider, this.plugin.opts.companionUrl) ) + this.plugin.uppy.addFiles(tagFiles) + this.resetPluginState() } diff --git a/packages/@uppy/provider-views/src/View.ts b/packages/@uppy/provider-views/src/View.ts index fc14966df2..1571a990f7 100644 --- a/packages/@uppy/provider-views/src/View.ts +++ b/packages/@uppy/provider-views/src/View.ts @@ -7,8 +7,6 @@ import type { } from '@uppy/core/lib/Uppy' import type { Body, Meta, TagFile } from '@uppy/utils/lib/UppyFile' import type { CompanionFile } from '@uppy/utils/lib/CompanionFile' -import getFileType from '@uppy/utils/lib/getFileType' -import isPreviewSupported from '@uppy/utils/lib/isPreviewSupported' import remoteFileObjToLocal from '@uppy/utils/lib/remoteFileObjToLocal' import type { RestrictionError } from '@uppy/core/lib/Restricter' import PartialTreeUtils from './utils/PartialTreeUtils' @@ -145,60 +143,6 @@ export default class View< this.plugin.uppy.registerRequestClient(this.provider.provider, this.provider) } - // TODO: document what is a "tagFile" or get rid of this concept - getTagFile(file: CompanionFile): TagFile { - const tagFile: TagFile = { - id: file.id, - source: this.plugin.id, - name: file.name || file.id, - type: file.mimeType, - isRemote: true, - data: file, - // @ts-expect-error meta is filled conditionally below - meta: {}, - body: { - fileId: file.id, - }, - remote: { - companionUrl: this.plugin.opts.companionUrl, - // @ts-expect-error untyped for now - url: `${this.provider.fileUrl(file.requestPath)}`, - body: { - fileId: file.id, - }, - providerName: this.provider.name, - provider: this.provider.provider, - requestClientId: this.provider.provider, - }, - } - - const fileType = getFileType(tagFile) - - // TODO Should we just always use the thumbnail URL if it exists? - if (fileType && isPreviewSupported(fileType)) { - tagFile.preview = file.thumbnail - } - - if (file.author) { - if (file.author.name != null) - tagFile.meta!.authorName = String(file.author.name) - 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 - } - filterItems = (items: PartialTree): PartialTree => { const { filterInput } = this.plugin.getPluginState() if (!filterInput || filterInput === '') { diff --git a/packages/@uppy/provider-views/src/utils/getTagFile.ts b/packages/@uppy/provider-views/src/utils/getTagFile.ts new file mode 100644 index 0000000000..8f5c670299 --- /dev/null +++ b/packages/@uppy/provider-views/src/utils/getTagFile.ts @@ -0,0 +1,60 @@ +import type { CompanionClientProvider, CompanionClientSearchProvider } from "@uppy/utils/lib/CompanionClientProvider" +import type { CompanionFile } from "@uppy/utils/lib/CompanionFile" +import type { Meta, TagFile } from "@uppy/utils/lib/UppyFile" +import getFileType from "@uppy/utils/lib/getFileType" +import isPreviewSupported from "@uppy/utils/lib/isPreviewSupported" + +// TODO: document what is a "tagFile" or get rid of this concept +const getTagFile = (file: CompanionFile, pluginId: string, provider: CompanionClientProvider | CompanionClientSearchProvider, companionUrl: string) : TagFile => { + const tagFile: TagFile = { + id: file.id, + source: pluginId, + name: file.name || file.id, + type: file.mimeType, + isRemote: true, + data: file, + meta: {}, + body: { + fileId: file.id, + }, + remote: { + companionUrl, + // @ts-expect-error untyped for now + url: `${provider.fileUrl(file.requestPath)}`, + body: { + fileId: file.id, + }, + providerName: provider.name, + provider: provider.provider, + requestClientId: provider.provider, + }, + } + + const fileType = getFileType(tagFile) + + // TODO Should we just always use the thumbnail URL if it exists? + if (fileType && isPreviewSupported(fileType)) { + tagFile.preview = file.thumbnail + } + + if (file.author) { + if (file.author.name != null) + tagFile.meta!.authorName = String(file.author.name) + 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 +} + +export default getTagFile From 503242b4ab79a704957cf93ede308b03f6676763 Mon Sep 17 00:00:00 2001 From: Evgenia Karunus Date: Wed, 17 Apr 2024 17:47:13 +0400 Subject: [PATCH 057/170] `recordShiftKeyPress()` - fix chaotic shift-clicking in Grid providers, remove endless prop drilling while we're at it --- packages/@uppy/provider-views/src/Browser.tsx | 4 ---- .../src/Item/components/GridLi.tsx | 5 ----- .../src/Item/components/ListLi.tsx | 4 ---- .../@uppy/provider-views/src/Item/index.tsx | 4 +--- .../src/ProviderView/ProviderView.tsx | 3 +-- .../SearchProviderView/SearchProviderView.tsx | 3 +-- packages/@uppy/provider-views/src/View.ts | 17 +++++++++++++---- 7 files changed, 16 insertions(+), 24 deletions(-) diff --git a/packages/@uppy/provider-views/src/Browser.tsx b/packages/@uppy/provider-views/src/Browser.tsx index e21de49436..b12e0d6a16 100644 --- a/packages/@uppy/provider-views/src/Browser.tsx +++ b/packages/@uppy/provider-views/src/Browser.tsx @@ -22,7 +22,6 @@ type BrowserProps = { headerComponent?: JSX.Element showBreadcrumbs: boolean toggleCheckbox: (event: Event, file: PartialTreeFile | PartialTreeFolderNode) => void - recordShiftKeyPress: (event: KeyboardEvent | MouseEvent) => void handleScroll: (event: Event) => Promise showTitles: boolean i18n: I18n @@ -52,7 +51,6 @@ function Browser( headerComponent, showBreadcrumbs, toggleCheckbox, - recordShiftKeyPress, handleScroll, showTitles, i18n, @@ -131,7 +129,6 @@ function Browser( ( = { itemIconEl: any showTitles?: boolean toggleCheckbox: (event: Event) => void - recordShiftKeyPress: (event: KeyboardEvent) => void id: string children?: JSX.Element } @@ -31,7 +30,6 @@ function GridListItem( itemIconEl, showTitles, toggleCheckbox, - recordShiftKeyPress, id, children, } = props @@ -45,9 +43,6 @@ function GridListItem( type="checkbox" className="uppy-u-reset uppy-ProviderBrowserItem-checkbox uppy-ProviderBrowserItem-checkbox--grid" onChange={toggleCheckbox} - onKeyDown={recordShiftKeyPress} - // @ts-expect-error this is fine onMouseDown too - onMouseDown={recordShiftKeyPress} name="listitem" id={id} checked={status === "checked" ? true : false} diff --git a/packages/@uppy/provider-views/src/Item/components/ListLi.tsx b/packages/@uppy/provider-views/src/Item/components/ListLi.tsx index 65ce66e930..f0b3013a65 100644 --- a/packages/@uppy/provider-views/src/Item/components/ListLi.tsx +++ b/packages/@uppy/provider-views/src/Item/components/ListLi.tsx @@ -18,7 +18,6 @@ type ListItemProps = { isCheckboxDisabled: boolean status: PartialTreeStatus toggleCheckbox: (event: Event) => void - recordShiftKeyPress: (event: KeyboardEvent | MouseEvent) => void type: string id: string itemIconEl: any @@ -38,7 +37,6 @@ export default function ListItem( isCheckboxDisabled, status, toggleCheckbox, - recordShiftKeyPress, type, id, itemIconEl, @@ -58,8 +56,6 @@ export default function ListItem( type="checkbox" className="uppy-u-reset uppy-ProviderBrowserItem-checkbox" onChange={toggleCheckbox} - onKeyDown={recordShiftKeyPress} - onMouseDown={recordShiftKeyPress} // for the name="listitem" id={id} diff --git a/packages/@uppy/provider-views/src/Item/index.tsx b/packages/@uppy/provider-views/src/Item/index.tsx index cec933423f..ce7a3a3321 100644 --- a/packages/@uppy/provider-views/src/Item/index.tsx +++ b/packages/@uppy/provider-views/src/Item/index.tsx @@ -16,7 +16,6 @@ const VIRTUAL_SHARED_DIR = 'shared-with-me' type ItemProps = { viewType: string toggleCheckbox: (event: Event, file: (PartialTreeFile | PartialTreeFolderNode)) => void - recordShiftKeyPress: (event: KeyboardEvent | MouseEvent) => void showTitles: boolean i18n: I18n validateRestrictions: (file: CompanionFile) => RestrictionError | null @@ -27,7 +26,7 @@ type ItemProps = { export default function Item( props: ItemProps, ): h.JSX.Element { - const { viewType, toggleCheckbox, recordShiftKeyPress, showTitles, i18n, validateRestrictions, getFolder, file } = props + const { viewType, toggleCheckbox, showTitles, i18n, validateRestrictions, getFolder, file } = props const restrictionError = validateRestrictions(file.data) const isDisabled = file.data.isFolder ? false : (Boolean(restrictionError) && (file.status !== "checked")) @@ -41,7 +40,6 @@ export default function Item( toggleCheckbox: (event: Event) => toggleCheckbox(event, file), viewType, showTitles, - recordShiftKeyPress, className: classNames( 'uppy-ProviderBrowserItem', { 'uppy-ProviderBrowserItem--disabled': isDisabled }, diff --git a/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx b/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx index b8aef0dd63..be2b8ca14f 100644 --- a/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx +++ b/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx @@ -325,7 +325,7 @@ export default class ProviderView extends View< const targetViewOptions = { ...this.opts, ...viewOptions } const { partialTree, currentFolderId, filterInput, loading } = this.plugin.getPluginState() - const { recordShiftKeyPress, filterItems } = this + const { filterItems } = this const pluginIcon = this.plugin.icon || defaultPickerIcon const headerProps = { @@ -343,7 +343,6 @@ export default class ProviderView extends View< const browserProps = { toggleCheckbox: this.toggleCheckbox.bind(this), - recordShiftKeyPress, displayedPartialTree, getFolder: this.getFolder, loadAllFiles: this.opts.loadAllFiles, diff --git a/packages/@uppy/provider-views/src/SearchProviderView/SearchProviderView.tsx b/packages/@uppy/provider-views/src/SearchProviderView/SearchProviderView.tsx index bb1a2d3b8d..270b48a2fc 100644 --- a/packages/@uppy/provider-views/src/SearchProviderView/SearchProviderView.tsx +++ b/packages/@uppy/provider-views/src/SearchProviderView/SearchProviderView.tsx @@ -179,13 +179,12 @@ export default class SearchProviderView< const targetViewOptions = { ...this.opts, ...viewOptions } const { loading, partialTree, currentFolderId } = this.plugin.getPluginState() - const { filterItems, recordShiftKeyPress } = this + const { filterItems } = this const displayedPartialTree = filterItems(partialTree.filter((item) => item.type !== 'root' && item.parentId === currentFolderId)) as PartialTreeFile[] const browserProps = { toggleCheckbox: this.toggleCheckbox.bind(this), - recordShiftKeyPress, displayedPartialTree, handleScroll: this.handleScroll, done: this.donePicking, diff --git a/packages/@uppy/provider-views/src/View.ts b/packages/@uppy/provider-views/src/View.ts index 1571a990f7..aabf757ffd 100644 --- a/packages/@uppy/provider-views/src/View.ts +++ b/packages/@uppy/provider-views/src/View.ts @@ -68,6 +68,19 @@ export default class View< this.cancelPicking = this.cancelPicking.bind(this) this.validateRestrictions = this.validateRestrictions.bind(this) this.getNOfSelectedFiles = this.getNOfSelectedFiles.bind(this) + + // This records whether the user is holding the SHIFT key this very moment. + // Typically this is implemented using `onClick((e) => e.shiftKey)` - but we can't use that, because for accessibility reasons we're using html tags that don't support `e.shiftKey` property (see #3768). + document.addEventListener('keyup', (e) => { + if (e.key == 'Shift') { + this.isShiftKeyPressed = false + } + }) + document.addEventListener('keydown', (e) => { + if (e.key == 'Shift') { + this.isShiftKeyPressed = true + } + }) } getNOfSelectedFiles () : number { @@ -157,10 +170,6 @@ export default class View< }) } - recordShiftKeyPress = (e: KeyboardEvent | MouseEvent): void => { - this.isShiftKeyPressed = e.shiftKey - } - /** * Toggles file/folder checkbox to on/off state while updating files list. * From 88c393b09a96c8e9fb9676b9184fb49bf554809a Mon Sep 17 00:00:00 2001 From: Evgenia Karunus Date: Wed, 17 Apr 2024 18:54:40 +0400 Subject: [PATCH 058/170] `getNOfSelectedFiles.ts`, `filterItems.ts` - factor out, this removes props drilling --- packages/@uppy/provider-views/src/Browser.tsx | 18 ++++++---- .../src/ProviderView/ProviderView.tsx | 8 ++--- .../SearchProviderView/SearchProviderView.tsx | 8 ++--- packages/@uppy/provider-views/src/View.ts | 36 ++----------------- .../src/utils/PartialTreeUtils.ts | 15 ++++---- .../provider-views/src/utils/filterItems.ts | 13 +++++++ .../src/utils/getNOfSelectedFiles.ts | 20 +++++++++++ 7 files changed, 62 insertions(+), 56 deletions(-) create mode 100644 packages/@uppy/provider-views/src/utils/filterItems.ts create mode 100644 packages/@uppy/provider-views/src/utils/getNOfSelectedFiles.ts diff --git a/packages/@uppy/provider-views/src/Browser.tsx b/packages/@uppy/provider-views/src/Browser.tsx index b12e0d6a16..b87887163b 100644 --- a/packages/@uppy/provider-views/src/Browser.tsx +++ b/packages/@uppy/provider-views/src/Browser.tsx @@ -11,12 +11,15 @@ import type Uppy from '@uppy/core' import SearchFilterInput from './SearchFilterInput.tsx' import FooterActions from './FooterActions.tsx' import Item from './Item/index.tsx' -import type { PartialTreeFile, PartialTreeFolderNode } from '@uppy/core/lib/Uppy.ts' +import type { PartialTree, PartialTreeFile, PartialTreeFolderNode } from '@uppy/core/lib/Uppy.ts' import type { RestrictionError } from '@uppy/core/lib/Restricter.ts' import type { CompanionFile } from '@uppy/utils/lib/CompanionFile' +import getNOfSelectedFiles from './utils/getNOfSelectedFiles.ts' +import filterItems from './utils/filterItems.ts' type BrowserProps = { - displayedPartialTree: (PartialTreeFile | PartialTreeFolderNode)[], + partialTree: PartialTree, + currentFolderId: string | null, viewType: string headerComponent?: JSX.Element @@ -26,11 +29,10 @@ type BrowserProps = { showTitles: boolean i18n: I18n validateRestrictions: (file: CompanionFile) => RestrictionError | null - getNOfSelectedFiles: () => number isLoading: boolean | string showSearchFilter: boolean search: (query: string) => void - searchTerm?: string | null + searchTerm: string clearSearch: () => void searchOnInput: boolean searchInputLabel: string @@ -46,7 +48,8 @@ function Browser( props: BrowserProps, ): JSX.Element { const { - displayedPartialTree, + partialTree, + currentFolderId, viewType, headerComponent, showBreadcrumbs, @@ -70,7 +73,10 @@ function Browser( loadAllFiles, } = props - const nOfSelectedFiles = props.getNOfSelectedFiles(); // TODO// currentSelection.length + const itemsInThisFolder = partialTree.filter((item) => item.type !== 'root' && item.parentId === currentFolderId) + const displayedPartialTree = filterItems(itemsInThisFolder, searchTerm) as (PartialTreeFile | PartialTreeFolderNode)[] + + const nOfSelectedFiles = getNOfSelectedFiles(partialTree) return (
            extends View< const targetViewOptions = { ...this.opts, ...viewOptions } const { partialTree, currentFolderId, filterInput, loading } = this.plugin.getPluginState() - const { filterItems } = this const pluginIcon = this.plugin.icon || defaultPickerIcon const headerProps = { @@ -339,11 +339,10 @@ export default class ProviderView extends View< i18n, } - const displayedPartialTree = filterItems(partialTree.filter((item) => item.type !== 'root' && item.parentId === currentFolderId)) as (PartialTreeFile | PartialTreeFolderNode)[] - const browserProps = { toggleCheckbox: this.toggleCheckbox.bind(this), - displayedPartialTree, + partialTree, + currentFolderId, getFolder: this.getFolder, loadAllFiles: this.opts.loadAllFiles, @@ -371,7 +370,6 @@ export default class ProviderView extends View< i18n: this.plugin.uppy.i18n, validateRestrictions: this.validateRestrictions, - getNOfSelectedFiles: this.getNOfSelectedFiles, isLoading: loading, } diff --git a/packages/@uppy/provider-views/src/SearchProviderView/SearchProviderView.tsx b/packages/@uppy/provider-views/src/SearchProviderView/SearchProviderView.tsx index 270b48a2fc..108dc3b346 100644 --- a/packages/@uppy/provider-views/src/SearchProviderView/SearchProviderView.tsx +++ b/packages/@uppy/provider-views/src/SearchProviderView/SearchProviderView.tsx @@ -14,6 +14,7 @@ import View, { type ViewOptions } from '../View.ts' // @ts-ignore We don't want TS to generate types for the package.json import packageJson from '../../package.json' import getTagFile from '../utils/getTagFile.ts' +import filterItems from '../utils/filterItems.ts' const defaultState : Partial = { isInputMode: true, @@ -179,13 +180,11 @@ export default class SearchProviderView< const targetViewOptions = { ...this.opts, ...viewOptions } const { loading, partialTree, currentFolderId } = this.plugin.getPluginState() - const { filterItems } = this - - const displayedPartialTree = filterItems(partialTree.filter((item) => item.type !== 'root' && item.parentId === currentFolderId)) as PartialTreeFile[] const browserProps = { toggleCheckbox: this.toggleCheckbox.bind(this), - displayedPartialTree, + partialTree, + currentFolderId, handleScroll: this.handleScroll, done: this.donePicking, cancel: this.cancelPicking, @@ -210,7 +209,6 @@ export default class SearchProviderView< pluginIcon: this.plugin.icon, i18n, validateRestrictions: this.validateRestrictions, - getNOfSelectedFiles: this.getNOfSelectedFiles } if (isInputMode) { diff --git a/packages/@uppy/provider-views/src/View.ts b/packages/@uppy/provider-views/src/View.ts index aabf757ffd..f7b7c63fe2 100644 --- a/packages/@uppy/provider-views/src/View.ts +++ b/packages/@uppy/provider-views/src/View.ts @@ -67,7 +67,6 @@ export default class View< this.handleError = this.handleError.bind(this) this.cancelPicking = this.cancelPicking.bind(this) this.validateRestrictions = this.validateRestrictions.bind(this) - this.getNOfSelectedFiles = this.getNOfSelectedFiles.bind(this) // This records whether the user is holding the SHIFT key this very moment. // Typically this is implemented using `onClick((e) => e.shiftKey)` - but we can't use that, because for accessibility reasons we're using html tags that don't support `e.shiftKey` property (see #3768). @@ -83,23 +82,6 @@ export default class View< }) } - getNOfSelectedFiles () : number { - const { partialTree } = this.plugin.getPluginState() - // We're interested in all 'checked' leaves. - const checkedLeaves = partialTree.filter((item) => { - if (item.type === 'file' && item.status === 'checked') { - return true - } else if (item.type === 'folder' && item.status === 'checked') { - const doesItHaveChildren = partialTree.some((i) => - i.type !== 'root' && i.parentId === item.id - ) - return !doesItHaveChildren - } - return false - }) - return checkedLeaves.length - } - validateRestrictions (file: CompanionFile) : RestrictionError | null { if (file.isFolder) return null @@ -156,20 +138,6 @@ export default class View< this.plugin.uppy.registerRequestClient(this.provider.provider, this.provider) } - filterItems = (items: PartialTree): PartialTree => { - const { filterInput } = this.plugin.getPluginState() - if (!filterInput || filterInput === '') { - return items - } - return items.filter((item) => { - return ( - item.type !== 'root' && - item.data.name.toLowerCase().indexOf(filterInput.toLowerCase()) !== - -1 - ) - }) - } - /** * Toggles file/folder checkbox to on/off state while updating files list. * @@ -183,9 +151,9 @@ export default class View< // Prevent shift-clicking from highlighting file names (https://stackoverflow.com/a/1527797/3192470) document.getSelection()?.removeAllRanges() - const { partialTree, currentFolderId } = this.plugin.getPluginState() + const { partialTree, currentFolderId, filterInput } = this.plugin.getPluginState() - const newPartialTree = PartialTreeUtils.afterToggleCheckbox(partialTree, ourItem, this.validateRestrictions, this.filterItems, currentFolderId, this.isShiftKeyPressed, this.lastCheckbox) + const newPartialTree = PartialTreeUtils.afterToggleCheckbox(partialTree, ourItem, this.validateRestrictions, filterInput, currentFolderId, this.isShiftKeyPressed, this.lastCheckbox) this.plugin.setPluginState({ partialTree: newPartialTree }) this.lastCheckbox = ourItem.id! diff --git a/packages/@uppy/provider-views/src/utils/PartialTreeUtils.ts b/packages/@uppy/provider-views/src/utils/PartialTreeUtils.ts index 9354bb53e3..e9591355db 100644 --- a/packages/@uppy/provider-views/src/utils/PartialTreeUtils.ts +++ b/packages/@uppy/provider-views/src/utils/PartialTreeUtils.ts @@ -1,11 +1,12 @@ import type { PartialTree, PartialTreeFile, PartialTreeFolder, PartialTreeFolderNode } from "@uppy/core/lib/Uppy" import type { CompanionFile } from "@uppy/utils/lib/CompanionFile" +import filterItems from "./filterItems" const afterToggleCheckbox = ( oldPartialTree: PartialTree, ourItem: PartialTreeFolderNode | PartialTreeFile, validateRestrictions: (file: CompanionFile) => object | null, - filterItems : (items: PartialTree) => PartialTree, + filterInput : string, currentFolderId: string | null, isShiftKeyPressed: boolean, lastCheckbox: string | undefined @@ -50,13 +51,15 @@ const afterToggleCheckbox = ( // Shift-clicking selects a single consecutive list of items // starting at the previous click. - const inThisFolder = filterItems(newPartialTree.filter((item) => item.type !== 'root' && item.parentId === currentFolderId)) as (PartialTreeFile | PartialTreeFolderNode)[] - const prevIndex = inThisFolder.findIndex((item) => item.id === lastCheckbox) + const itemsInThisFolder = newPartialTree.filter((item) => item.type !== 'root' && item.parentId === currentFolderId) + const visibleItems = filterItems(itemsInThisFolder, filterInput) as (PartialTreeFile | PartialTreeFolderNode)[] + + const prevIndex = visibleItems.findIndex((item) => item.id === lastCheckbox) if (prevIndex !== -1 && isShiftKeyPressed) { - const newIndex = inThisFolder.findIndex((item) => item.id === ourItem.id) + const newIndex = visibleItems.findIndex((item) => item.id === ourItem.id) const toMarkAsChecked = (prevIndex < newIndex ? - inThisFolder.slice(prevIndex, newIndex + 1) - : inThisFolder.slice(newIndex, prevIndex + 1) + visibleItems.slice(prevIndex, newIndex + 1) + : visibleItems.slice(newIndex, prevIndex + 1) ).map((item) => item.id) const newlyCheckedItems = newPartialTree diff --git a/packages/@uppy/provider-views/src/utils/filterItems.ts b/packages/@uppy/provider-views/src/utils/filterItems.ts new file mode 100644 index 0000000000..17c36ff5a5 --- /dev/null +++ b/packages/@uppy/provider-views/src/utils/filterItems.ts @@ -0,0 +1,13 @@ +import type { PartialTree } from "@uppy/core/lib/Uppy" + +const filterItems = (items: PartialTree, filterInput: string | undefined): PartialTree => { + if (!filterInput || filterInput === '') { + return items + } + return items.filter((item) => + item.type !== 'root' && + item.data.name.toLowerCase().indexOf(filterInput.toLowerCase()) !== -1 + ) +} + +export default filterItems diff --git a/packages/@uppy/provider-views/src/utils/getNOfSelectedFiles.ts b/packages/@uppy/provider-views/src/utils/getNOfSelectedFiles.ts new file mode 100644 index 0000000000..9a17f79c94 --- /dev/null +++ b/packages/@uppy/provider-views/src/utils/getNOfSelectedFiles.ts @@ -0,0 +1,20 @@ +import type { PartialTree } from "@uppy/core/lib/Uppy" + +// We're interested in all 'checked' leaves of this tree - +// I believe it's the most intuitive number we can show to the user given we don't have full information about how many files are inside of each selected folder. +const getNOfSelectedFiles = (partialTree : PartialTree) : number => { + const checkedLeaves = partialTree.filter((item) => { + if (item.type === 'file' && item.status === 'checked') { + return true + } else if (item.type === 'folder' && item.status === 'checked') { + const doesItHaveChildren = partialTree.some((i) => + i.type !== 'root' && i.parentId === item.id + ) + return !doesItHaveChildren + } + return false + }) + return checkedLeaves.length +} + +export default getNOfSelectedFiles From 8eff1dfae5df22626b52c015dedba0601ec05cd7 Mon Sep 17 00:00:00 2001 From: Evgenia Karunus Date: Thu, 18 Apr 2024 18:40:27 +0400 Subject: [PATCH 059/170] - pass `displayedPartialTree` right away (because Search&NormalProvider have wildly different logics!) --- packages/@uppy/provider-views/src/Browser.tsx | 18 +++++------------- .../src/ProviderView/ProviderView.tsx | 8 ++++++-- .../SearchProviderView/SearchProviderView.tsx | 4 +++- 3 files changed, 14 insertions(+), 16 deletions(-) diff --git a/packages/@uppy/provider-views/src/Browser.tsx b/packages/@uppy/provider-views/src/Browser.tsx index b87887163b..3b3c140187 100644 --- a/packages/@uppy/provider-views/src/Browser.tsx +++ b/packages/@uppy/provider-views/src/Browser.tsx @@ -5,21 +5,18 @@ import classNames from 'classnames' // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore untyped import VirtualList from '@uppy/utils/lib/VirtualList' -import type { Body, Meta, UppyFile } from '@uppy/utils/lib/UppyFile' +import type { Body, Meta } from '@uppy/utils/lib/UppyFile' import type { I18n } from '@uppy/utils/lib/Translator' -import type Uppy from '@uppy/core' import SearchFilterInput from './SearchFilterInput.tsx' import FooterActions from './FooterActions.tsx' import Item from './Item/index.tsx' import type { PartialTree, PartialTreeFile, PartialTreeFolderNode } from '@uppy/core/lib/Uppy.ts' import type { RestrictionError } from '@uppy/core/lib/Restricter.ts' import type { CompanionFile } from '@uppy/utils/lib/CompanionFile' -import getNOfSelectedFiles from './utils/getNOfSelectedFiles.ts' -import filterItems from './utils/filterItems.ts' type BrowserProps = { - partialTree: PartialTree, - currentFolderId: string | null, + displayedPartialTree: (PartialTreeFile | PartialTreeFolderNode)[], + nOfSelectedFiles: number, viewType: string headerComponent?: JSX.Element @@ -48,8 +45,8 @@ function Browser( props: BrowserProps, ): JSX.Element { const { - partialTree, - currentFolderId, + displayedPartialTree, + nOfSelectedFiles, viewType, headerComponent, showBreadcrumbs, @@ -73,11 +70,6 @@ function Browser( loadAllFiles, } = props - const itemsInThisFolder = partialTree.filter((item) => item.type !== 'root' && item.parentId === currentFolderId) - const displayedPartialTree = filterItems(itemsInThisFolder, searchTerm) as (PartialTreeFile | PartialTreeFolderNode)[] - - const nOfSelectedFiles = getNOfSelectedFiles(partialTree) - return (
            extends View< i18n, } + const itemsInThisFolder = partialTree.filter((item) => item.type !== 'root' && item.parentId === currentFolderId) + const displayedPartialTree = filterItems(itemsInThisFolder, filterInput) as (PartialTreeFile | PartialTreeFolderNode)[] + const browserProps = { toggleCheckbox: this.toggleCheckbox.bind(this), - partialTree, - currentFolderId, + displayedPartialTree, + nOfSelectedFiles: getNOfSelectedFiles(partialTree), getFolder: this.getFolder, loadAllFiles: this.opts.loadAllFiles, diff --git a/packages/@uppy/provider-views/src/SearchProviderView/SearchProviderView.tsx b/packages/@uppy/provider-views/src/SearchProviderView/SearchProviderView.tsx index 108dc3b346..6004df06b9 100644 --- a/packages/@uppy/provider-views/src/SearchProviderView/SearchProviderView.tsx +++ b/packages/@uppy/provider-views/src/SearchProviderView/SearchProviderView.tsx @@ -15,6 +15,7 @@ import View, { type ViewOptions } from '../View.ts' import packageJson from '../../package.json' import getTagFile from '../utils/getTagFile.ts' import filterItems from '../utils/filterItems.ts' +import getNOfSelectedFiles from '../utils/getNOfSelectedFiles.ts' const defaultState : Partial = { isInputMode: true, @@ -183,7 +184,8 @@ export default class SearchProviderView< const browserProps = { toggleCheckbox: this.toggleCheckbox.bind(this), - partialTree, + displayedPartialTree: partialTree.filter((item) => item.type !== 'root' && item.parentId === currentFolderId), + nOfSelectedFiles: getNOfSelectedFiles(partialTree), currentFolderId, handleScroll: this.handleScroll, done: this.donePicking, From c09105ac0cb4d70dbb7e3dc764723955b0efc485 Mon Sep 17 00:00:00 2001 From: Evgenia Karunus Date: Thu, 18 Apr 2024 18:59:42 +0400 Subject: [PATCH 060/170] `searchTerm`, `filterInput` - we only need one of these of course! --- packages/@uppy/core/src/Uppy.ts | 5 ++-- packages/@uppy/provider-views/src/Browser.tsx | 6 ++--- .../src/ProviderView/ProviderView.tsx | 16 ++++++------ .../SearchProviderView/SearchProviderView.tsx | 25 ++++++++----------- 4 files changed, 24 insertions(+), 28 deletions(-) diff --git a/packages/@uppy/core/src/Uppy.ts b/packages/@uppy/core/src/Uppy.ts index 6dab9740ee..782fb799aa 100644 --- a/packages/@uppy/core/src/Uppy.ts +++ b/packages/@uppy/core/src/Uppy.ts @@ -110,7 +110,7 @@ export type PartialTree = (PartialTreeFile | PartialTreeFolder)[] export type UnknownProviderPluginState = { authenticated: boolean | undefined didFirstRender: boolean - filterInput: string + searchString: string loading: boolean | string partialTree: PartialTree currentFolderId: string | null @@ -151,11 +151,10 @@ export type UnknownProviderPlugin< */ export type UnknownSearchProviderPluginState = { isInputMode?: boolean - searchTerm?: string | null } & Pick< UnknownProviderPluginState, | 'loading' - | 'filterInput' + | 'searchString' | 'didFirstRender' | 'partialTree' | 'currentFolderId' diff --git a/packages/@uppy/provider-views/src/Browser.tsx b/packages/@uppy/provider-views/src/Browser.tsx index 3b3c140187..62c5ddb8dd 100644 --- a/packages/@uppy/provider-views/src/Browser.tsx +++ b/packages/@uppy/provider-views/src/Browser.tsx @@ -29,7 +29,7 @@ type BrowserProps = { isLoading: boolean | string showSearchFilter: boolean search: (query: string) => void - searchTerm: string + searchString: string clearSearch: () => void searchOnInput: boolean searchInputLabel: string @@ -58,7 +58,7 @@ function Browser( isLoading, showSearchFilter, search, - searchTerm, + searchString, clearSearch, searchOnInput, searchInputLabel, @@ -94,7 +94,7 @@ function Browser(
            extends View< const clickedFolder = partialTree.find((folder) => folder.id === folderId)! as PartialTreeFolder if (clickedFolder.cached) { console.log("Folder was cached____________________________________________"); - this.plugin.setPluginState({ currentFolderId: folderId, filterInput: '' }) + this.plugin.setPluginState({ currentFolderId: folderId, searchString: '' }) return } @@ -188,7 +188,7 @@ export default class ProviderView extends View< this.plugin.setPluginState({ partialTree: newPartialTree, currentFolderId: folderId, - filterInput: '' + searchString: '' }) }).catch(this.handleError) @@ -325,7 +325,7 @@ export default class ProviderView extends View< } const targetViewOptions = { ...this.opts, ...viewOptions } - const { partialTree, currentFolderId, filterInput, loading } = + const { partialTree, currentFolderId, searchString, loading } = this.plugin.getPluginState() const pluginIcon = this.plugin.icon || defaultPickerIcon @@ -341,7 +341,7 @@ export default class ProviderView extends View< } const itemsInThisFolder = partialTree.filter((item) => item.type !== 'root' && item.parentId === currentFolderId) - const displayedPartialTree = filterItems(itemsInThisFolder, filterInput) as (PartialTreeFile | PartialTreeFolderNode)[] + const displayedPartialTree = filterItems(itemsInThisFolder, searchString) as (PartialTreeFile | PartialTreeFolderNode)[] const browserProps = { toggleCheckbox: this.toggleCheckbox.bind(this), @@ -352,9 +352,9 @@ export default class ProviderView extends View< // For SearchFilterInput component showSearchFilter: targetViewOptions.showFilter, - search: (input: string | undefined) => this.plugin.setPluginState({ filterInput: input }), - clearSearch: () => this.plugin.setPluginState({ filterInput: '' }), - searchTerm: filterInput, + search: (input: string | undefined) => this.plugin.setPluginState({ searchString: input }), + clearSearch: () => this.plugin.setPluginState({ searchString: '' }), + searchString, searchOnInput: true, searchInputLabel: i18n('filter'), clearSearchLabel: i18n('resetFilter'), diff --git a/packages/@uppy/provider-views/src/SearchProviderView/SearchProviderView.tsx b/packages/@uppy/provider-views/src/SearchProviderView/SearchProviderView.tsx index 6004df06b9..f5a0ebb352 100644 --- a/packages/@uppy/provider-views/src/SearchProviderView/SearchProviderView.tsx +++ b/packages/@uppy/provider-views/src/SearchProviderView/SearchProviderView.tsx @@ -1,9 +1,8 @@ import { h } from 'preact' import type { Body, Meta } from '@uppy/utils/lib/UppyFile' -import type { PartialTree, PartialTreeFile, PartialTreeFolderNode, PartialTreeStatusFile, UnknownSearchProviderPlugin, UnknownSearchProviderPluginState } from '@uppy/core/lib/Uppy.ts' +import type { PartialTree, PartialTreeFile, PartialTreeFolderNode, UnknownSearchProviderPlugin, UnknownSearchProviderPluginState } from '@uppy/core/lib/Uppy.ts' import type { DefinePluginOpts } from '@uppy/core/lib/BasePlugin.ts' -import type Uppy from '@uppy/core' import type { CompanionFile } from '@uppy/utils/lib/CompanionFile' import SearchFilterInput from '../SearchFilterInput.tsx' import Browser from '../Browser.tsx' @@ -14,13 +13,11 @@ import View, { type ViewOptions } from '../View.ts' // @ts-ignore We don't want TS to generate types for the package.json import packageJson from '../../package.json' import getTagFile from '../utils/getTagFile.ts' -import filterItems from '../utils/filterItems.ts' import getNOfSelectedFiles from '../utils/getNOfSelectedFiles.ts' const defaultState : Partial = { isInputMode: true, - filterInput: '', - searchTerm: null, + searchString: '', partialTree: [ { type: 'root', @@ -109,13 +106,13 @@ export default class SearchProviderView< this.plugin.setPluginState({ partialTree: newPartialTree, isInputMode: false, - searchTerm: res.searchedFor, + searchString: res.searchedFor, }) } async search(query: string): Promise { - const { searchTerm } = this.plugin.getPluginState() - if (query && query === searchTerm) { + const { searchString } = this.plugin.getPluginState() + if (query && query === searchString) { // no need to search again as this is the same as the previous search return } @@ -135,7 +132,7 @@ export default class SearchProviderView< this.plugin.setPluginState({ partialTree: [], currentFolderId: null, - searchTerm: null, + searchString: '', }) } @@ -146,8 +143,8 @@ export default class SearchProviderView< this.isHandlingScroll = true try { - const { searchTerm } = this.plugin.getPluginState() - const response = await this.provider.search(searchTerm!, query) + const { searchString } = this.plugin.getPluginState() + const response = await this.provider.search(searchString, query) this.#updateFilesAndInputMode(response) } catch (error) { @@ -174,7 +171,7 @@ export default class SearchProviderView< state: unknown, viewOptions: Omit, 'provider'> = {}, ): JSX.Element { - const { isInputMode, searchTerm } = + const { isInputMode, searchString } = this.plugin.getPluginState() const { i18n } = this.plugin.uppy @@ -184,7 +181,7 @@ export default class SearchProviderView< const browserProps = { toggleCheckbox: this.toggleCheckbox.bind(this), - displayedPartialTree: partialTree.filter((item) => item.type !== 'root' && item.parentId === currentFolderId), + displayedPartialTree: partialTree.filter((item) => item.type !== 'root' && item.parentId === currentFolderId) as (PartialTreeFolderNode | PartialTreeFile)[], nOfSelectedFiles: getNOfSelectedFiles(partialTree), currentFolderId, handleScroll: this.handleScroll, @@ -196,7 +193,7 @@ export default class SearchProviderView< showSearchFilter: targetViewOptions.showFilter, search: this.search, clearSearch: this.clearSearch, - searchTerm, + searchString, searchOnInput: false, searchInputLabel: i18n('search'), clearSearchLabel: i18n('resetSearch'), From 1302640ddd01cb055e0d64dfd75d2a395521d29a Mon Sep 17 00:00:00 2001 From: Evgenia Karunus Date: Thu, 18 Apr 2024 19:36:06 +0400 Subject: [PATCH 061/170] - fix the issue where `afterToggleCheckbox()` thinks we should always filter by `searchString` --- .../src/ProviderView/ProviderView.tsx | 17 ++++++++++++ .../SearchProviderView/SearchProviderView.tsx | 17 ++++++++++++ packages/@uppy/provider-views/src/View.ts | 26 +------------------ .../src/utils/PartialTreeUtils.ts | 15 ++++------- 4 files changed, 40 insertions(+), 35 deletions(-) diff --git a/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx b/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx index caa583e01b..9dbf431113 100644 --- a/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx +++ b/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx @@ -311,6 +311,23 @@ export default class ProviderView extends View< return breadcrumbs.toReversed() } + toggleCheckbox(e: Event, ourItem: PartialTreeFolderNode | PartialTreeFile) { + e.stopPropagation() + e.preventDefault() + // Prevent shift-clicking from highlighting file names + // (https://stackoverflow.com/a/1527797/3192470) + document.getSelection()?.removeAllRanges() + + const { partialTree, currentFolderId, searchString } = this.plugin.getPluginState() + + const itemsInThisFolder = partialTree.filter((item) => item.type !== 'root' && item.parentId === currentFolderId) + const displayedPartialTree = filterItems(itemsInThisFolder, searchString) as (PartialTreeFile | PartialTreeFolderNode)[] + const newPartialTree = PartialTreeUtils.afterToggleCheckbox(partialTree, displayedPartialTree, ourItem, this.validateRestrictions, this.isShiftKeyPressed, this.lastCheckbox) + + this.plugin.setPluginState({ partialTree: newPartialTree }) + this.lastCheckbox = ourItem.id! + } + render( state: unknown, viewOptions: Omit, 'provider'> = {}, diff --git a/packages/@uppy/provider-views/src/SearchProviderView/SearchProviderView.tsx b/packages/@uppy/provider-views/src/SearchProviderView/SearchProviderView.tsx index f5a0ebb352..7fd68e5e26 100644 --- a/packages/@uppy/provider-views/src/SearchProviderView/SearchProviderView.tsx +++ b/packages/@uppy/provider-views/src/SearchProviderView/SearchProviderView.tsx @@ -14,6 +14,7 @@ import View, { type ViewOptions } from '../View.ts' import packageJson from '../../package.json' import getTagFile from '../utils/getTagFile.ts' import getNOfSelectedFiles from '../utils/getNOfSelectedFiles.ts' +import PartialTreeUtils from '../utils/PartialTreeUtils.ts' const defaultState : Partial = { isInputMode: true, @@ -167,6 +168,22 @@ export default class SearchProviderView< this.resetPluginState() } + toggleCheckbox(e: Event, ourItem: PartialTreeFolderNode | PartialTreeFile) { + e.stopPropagation() + e.preventDefault() + // Prevent shift-clicking from highlighting file names + // (https://stackoverflow.com/a/1527797/3192470) + document.getSelection()?.removeAllRanges() + + const { partialTree, currentFolderId, searchString } = this.plugin.getPluginState() + + const displayedPartialTree = partialTree.filter((item) => item.type !== 'root' && item.parentId === currentFolderId) as (PartialTreeFolderNode | PartialTreeFile)[] + const newPartialTree = PartialTreeUtils.afterToggleCheckbox(partialTree, displayedPartialTree, ourItem, this.validateRestrictions, this.isShiftKeyPressed, this.lastCheckbox) + + this.plugin.setPluginState({ partialTree: newPartialTree }) + this.lastCheckbox = ourItem.id! + } + render( state: unknown, viewOptions: Omit, 'provider'> = {}, diff --git a/packages/@uppy/provider-views/src/View.ts b/packages/@uppy/provider-views/src/View.ts index f7b7c63fe2..a9a74dd60d 100644 --- a/packages/@uppy/provider-views/src/View.ts +++ b/packages/@uppy/provider-views/src/View.ts @@ -1,15 +1,12 @@ import type { - PartialTree, PartialTreeFile, - PartialTreeFolderNode, UnknownProviderPlugin, UnknownSearchProviderPlugin, } from '@uppy/core/lib/Uppy' -import type { Body, Meta, TagFile } from '@uppy/utils/lib/UppyFile' +import type { Body, Meta } from '@uppy/utils/lib/UppyFile' import type { CompanionFile } from '@uppy/utils/lib/CompanionFile' import remoteFileObjToLocal from '@uppy/utils/lib/remoteFileObjToLocal' import type { RestrictionError } from '@uppy/core/lib/Restricter' -import PartialTreeUtils from './utils/PartialTreeUtils' type PluginType = 'Provider' | 'SearchProvider' @@ -138,27 +135,6 @@ export default class View< this.plugin.uppy.registerRequestClient(this.provider.provider, this.provider) } - /** - * 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: Event, ourItem: PartialTreeFolderNode | PartialTreeFile) { - e.stopPropagation() - e.preventDefault() - // Prevent shift-clicking from highlighting file names (https://stackoverflow.com/a/1527797/3192470) - document.getSelection()?.removeAllRanges() - - const { partialTree, currentFolderId, filterInput } = this.plugin.getPluginState() - - const newPartialTree = PartialTreeUtils.afterToggleCheckbox(partialTree, ourItem, this.validateRestrictions, filterInput, currentFolderId, this.isShiftKeyPressed, this.lastCheckbox) - - this.plugin.setPluginState({ partialTree: newPartialTree }) - this.lastCheckbox = ourItem.id! - } - setLoading(loading: boolean | string): void { this.plugin.setPluginState({ loading }) } diff --git a/packages/@uppy/provider-views/src/utils/PartialTreeUtils.ts b/packages/@uppy/provider-views/src/utils/PartialTreeUtils.ts index e9591355db..6473f0948b 100644 --- a/packages/@uppy/provider-views/src/utils/PartialTreeUtils.ts +++ b/packages/@uppy/provider-views/src/utils/PartialTreeUtils.ts @@ -1,13 +1,11 @@ import type { PartialTree, PartialTreeFile, PartialTreeFolder, PartialTreeFolderNode } from "@uppy/core/lib/Uppy" import type { CompanionFile } from "@uppy/utils/lib/CompanionFile" -import filterItems from "./filterItems" const afterToggleCheckbox = ( oldPartialTree: PartialTree, + displayedPartialTree: (PartialTreeFolderNode | PartialTreeFile)[], ourItem: PartialTreeFolderNode | PartialTreeFile, validateRestrictions: (file: CompanionFile) => object | null, - filterInput : string, - currentFolderId: string | null, isShiftKeyPressed: boolean, lastCheckbox: string | undefined ) : PartialTree => { @@ -51,15 +49,12 @@ const afterToggleCheckbox = ( // Shift-clicking selects a single consecutive list of items // starting at the previous click. - const itemsInThisFolder = newPartialTree.filter((item) => item.type !== 'root' && item.parentId === currentFolderId) - const visibleItems = filterItems(itemsInThisFolder, filterInput) as (PartialTreeFile | PartialTreeFolderNode)[] - - const prevIndex = visibleItems.findIndex((item) => item.id === lastCheckbox) + const prevIndex = displayedPartialTree.findIndex((item) => item.id === lastCheckbox) if (prevIndex !== -1 && isShiftKeyPressed) { - const newIndex = visibleItems.findIndex((item) => item.id === ourItem.id) + const newIndex = displayedPartialTree.findIndex((item) => item.id === ourItem.id) const toMarkAsChecked = (prevIndex < newIndex ? - visibleItems.slice(prevIndex, newIndex + 1) - : visibleItems.slice(newIndex, prevIndex + 1) + displayedPartialTree.slice(prevIndex, newIndex + 1) + : displayedPartialTree.slice(newIndex, prevIndex + 1) ).map((item) => item.id) const newlyCheckedItems = newPartialTree From 72eee03cfb5b0e27abfc522a6628f06000141e8a Mon Sep 17 00:00:00 2001 From: Evgenia Karunus Date: Thu, 18 Apr 2024 20:30:57 +0400 Subject: [PATCH 062/170] - remove `this.nextPageQuery` Also: fix the issue where upon searching for "ocean" and then "pajama" would just be adding pajama pictures after the ocean ones --- .../SearchProviderView/SearchProviderView.tsx | 93 ++++++++++--------- 1 file changed, 50 insertions(+), 43 deletions(-) diff --git a/packages/@uppy/provider-views/src/SearchProviderView/SearchProviderView.tsx b/packages/@uppy/provider-views/src/SearchProviderView/SearchProviderView.tsx index 7fd68e5e26..dc36fe9a89 100644 --- a/packages/@uppy/provider-views/src/SearchProviderView/SearchProviderView.tsx +++ b/packages/@uppy/provider-views/src/SearchProviderView/SearchProviderView.tsx @@ -1,7 +1,7 @@ import { h } from 'preact' import type { Body, Meta } from '@uppy/utils/lib/UppyFile' -import type { PartialTree, PartialTreeFile, PartialTreeFolderNode, UnknownSearchProviderPlugin, UnknownSearchProviderPluginState } from '@uppy/core/lib/Uppy.ts' +import type { PartialTree, PartialTreeFile, PartialTreeFolderNode, PartialTreeFolderRoot, UnknownSearchProviderPlugin, UnknownSearchProviderPluginState } from '@uppy/core/lib/Uppy.ts' import type { DefinePluginOpts } from '@uppy/core/lib/BasePlugin.ts' import type { CompanionFile } from '@uppy/utils/lib/CompanionFile' import SearchFilterInput from '../SearchFilterInput.tsx' @@ -61,8 +61,6 @@ export default class SearchProviderView< > extends View> { static VERSION = packageJson.version - nextPageQuery: string | null = null - constructor( plugin: UnknownSearchProviderPlugin, opts: ViewOptions, @@ -91,42 +89,36 @@ export default class SearchProviderView< this.plugin.setPluginState(defaultState) } - #updateFilesAndInputMode(res: Res): void { - this.nextPageQuery = res.nextPageQuery - const { partialTree } = this.plugin.getPluginState() - const newPartialTree : PartialTree = [ - ...partialTree, - ...res.items.map((item) => ({ - type: 'file', - id: item.requestPath, - status: 'unchecked', - parentId: null, - data: item - }) as PartialTreeFile) - ] - this.plugin.setPluginState({ - partialTree: newPartialTree, - isInputMode: false, - searchString: res.searchedFor, - }) - } - - async search(query: string): Promise { - const { searchString } = this.plugin.getPluginState() - if (query && query === searchString) { - // no need to search again as this is the same as the previous search - return - } + async search(searchString: string): Promise { + this.plugin.setPluginState({ searchString }) this.setLoading(true) try { - const res = await this.provider.search(query) - this.#updateFilesAndInputMode(res) + const response = await this.provider.search(searchString) + + const newPartialTree : PartialTree = [ + { + type: 'root', + id: null, + cached: false, + nextPagePath: response.nextPageQuery + }, + ...response.items.map((item) => ({ + type: 'file', + id: item.requestPath, + status: 'unchecked', + parentId: null, + data: item + }) as PartialTreeFile) + ] + this.plugin.setPluginState({ + partialTree: newPartialTree, + isInputMode: false + }) } catch (err) { this.handleError(err) - } finally { - this.setLoading(false) } + this.setLoading(false) } clearSearch(): void { @@ -138,21 +130,36 @@ export default class SearchProviderView< } async handleScroll(event: Event): Promise { - const query = this.nextPageQuery || null + const { partialTree, searchString } = this.plugin.getPluginState() + const root = partialTree.find((i) => i.type === 'root') as PartialTreeFolderRoot - if (this.shouldHandleScroll(event) && query) { + if (this.shouldHandleScroll(event) && root.nextPagePath) { this.isHandlingScroll = true - try { - const { searchString } = this.plugin.getPluginState() - const response = await this.provider.search(searchString, query) - - this.#updateFilesAndInputMode(response) + const response = await this.provider.search(searchString, root.nextPagePath) + + const newRoot : PartialTreeFolderRoot = { + ...root, + nextPagePath: response.nextPageQuery + } + const oldItems = partialTree.filter((i) => i.type !== 'root') + + const newPartialTree : PartialTree = [ + newRoot, + ...oldItems, + ...response.items.map((item) => ({ + type: 'file', + id: item.requestPath, + status: 'unchecked', + parentId: null, + data: item + }) as PartialTreeFile) + ] + this.plugin.setPluginState({ partialTree: newPartialTree }) } catch (error) { this.handleError(error) - } finally { - this.isHandlingScroll = false } + this.isHandlingScroll = false } } @@ -175,7 +182,7 @@ export default class SearchProviderView< // (https://stackoverflow.com/a/1527797/3192470) document.getSelection()?.removeAllRanges() - const { partialTree, currentFolderId, searchString } = this.plugin.getPluginState() + const { partialTree, currentFolderId } = this.plugin.getPluginState() const displayedPartialTree = partialTree.filter((item) => item.type !== 'root' && item.parentId === currentFolderId) as (PartialTreeFolderNode | PartialTreeFile)[] const newPartialTree = PartialTreeUtils.afterToggleCheckbox(partialTree, displayedPartialTree, ourItem, this.validateRestrictions, this.isShiftKeyPressed, this.lastCheckbox) From 4dba9abef82f756756042170adfbb9764f74e984 Mon Sep 17 00:00:00 2001 From: Evgenia Karunus Date: Thu, 18 Apr 2024 20:39:32 +0400 Subject: [PATCH 063/170] - remove unnecessary prop indirection Typescript didn't actually know some of these props aren't used (removed those now)! It only discovers unused props upon normal props passing, like we do now. --- .../src/ProviderView/ProviderView.tsx | 87 +++++++++---------- .../SearchProviderView/SearchProviderView.tsx | 56 +++++------- 2 files changed, 63 insertions(+), 80 deletions(-) diff --git a/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx b/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx index 9dbf431113..1bd6a6020c 100644 --- a/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx +++ b/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx @@ -346,54 +346,9 @@ export default class ProviderView extends View< this.plugin.getPluginState() const pluginIcon = this.plugin.icon || defaultPickerIcon - const headerProps = { - showBreadcrumbs: targetViewOptions.showBreadcrumbs, - getFolder: this.getFolder, - breadcrumbs: this.getBreadcrumbs(), - pluginIcon, - title: this.plugin.title, - logout: this.logout, - username: this.username, - i18n, - } - const itemsInThisFolder = partialTree.filter((item) => item.type !== 'root' && item.parentId === currentFolderId) const displayedPartialTree = filterItems(itemsInThisFolder, searchString) as (PartialTreeFile | PartialTreeFolderNode)[] - const browserProps = { - toggleCheckbox: this.toggleCheckbox.bind(this), - displayedPartialTree, - nOfSelectedFiles: getNOfSelectedFiles(partialTree), - getFolder: this.getFolder, - loadAllFiles: this.opts.loadAllFiles, - - // For SearchFilterInput component - showSearchFilter: targetViewOptions.showFilter, - search: (input: string | undefined) => this.plugin.setPluginState({ searchString: input }), - clearSearch: () => this.plugin.setPluginState({ searchString: '' }), - searchString, - searchOnInput: true, - searchInputLabel: i18n('filter'), - clearSearchLabel: i18n('resetFilter'), - - noResultsLabel: i18n('noFilesFound'), - logout: this.logout, - handleScroll: this.handleScroll, - done: this.donePicking, - cancel: this.cancelPicking, - // eslint-disable-next-line react/jsx-props-no-spreading - headerComponent: {...headerProps} />, - title: this.plugin.title, - viewType: targetViewOptions.viewType, - showTitles: targetViewOptions.showTitles, - showBreadcrumbs: targetViewOptions.showBreadcrumbs, - pluginIcon, - i18n: this.plugin.uppy.i18n, - - validateRestrictions: this.validateRestrictions, - isLoading: loading, - } - if (authenticated === false) { return ( extends View< ) } - // eslint-disable-next-line react/jsx-props-no-spreading - return {...browserProps} /> + return + toggleCheckbox={this.toggleCheckbox.bind(this)} + displayedPartialTree={displayedPartialTree} + nOfSelectedFiles={getNOfSelectedFiles(partialTree)} + getFolder={this.getFolder} + loadAllFiles={this.opts.loadAllFiles} + + // For SearchFilterInput component + showSearchFilter={targetViewOptions.showFilter} + search={(input: string | undefined) => this.plugin.setPluginState({ searchString: input })} + clearSearch={() => this.plugin.setPluginState({ searchString: '' })} + searchString={searchString} + searchOnInput={true} + searchInputLabel={i18n('filter')} + clearSearchLabel={i18n('resetFilter')} + + noResultsLabel={i18n('noFilesFound')} + handleScroll={this.handleScroll} + done={this.donePicking} + cancel={this.cancelPicking} + headerComponent={ + + showBreadcrumbs={targetViewOptions.showBreadcrumbs} + getFolder={this.getFolder} + breadcrumbs={this.getBreadcrumbs} + pluginIcon={pluginIcon} + title={this.plugin.title} + logout={this.logout} + username={this.username} + i18n={i18n} + /> + } + viewType={targetViewOptions.viewType} + showTitles={targetViewOptions.showTitles} + showBreadcrumbs={targetViewOptions.showBreadcrumbs} + i18n={this.plugin.uppy.i18n} + + validateRestrictions={this.validateRestrictions} + isLoading={loading} + /> } } diff --git a/packages/@uppy/provider-views/src/SearchProviderView/SearchProviderView.tsx b/packages/@uppy/provider-views/src/SearchProviderView/SearchProviderView.tsx index dc36fe9a89..c2332b23a7 100644 --- a/packages/@uppy/provider-views/src/SearchProviderView/SearchProviderView.tsx +++ b/packages/@uppy/provider-views/src/SearchProviderView/SearchProviderView.tsx @@ -203,37 +203,6 @@ export default class SearchProviderView< const { loading, partialTree, currentFolderId } = this.plugin.getPluginState() - const browserProps = { - toggleCheckbox: this.toggleCheckbox.bind(this), - displayedPartialTree: partialTree.filter((item) => item.type !== 'root' && item.parentId === currentFolderId) as (PartialTreeFolderNode | PartialTreeFile)[], - nOfSelectedFiles: getNOfSelectedFiles(partialTree), - currentFolderId, - handleScroll: this.handleScroll, - done: this.donePicking, - cancel: this.cancelPicking, - getFolder: () => {}, - - // For SearchFilterInput component - showSearchFilter: targetViewOptions.showFilter, - search: this.search, - clearSearch: this.clearSearch, - searchString, - searchOnInput: false, - searchInputLabel: i18n('search'), - clearSearchLabel: i18n('resetSearch'), - - noResultsLabel: i18n('noSearchResults'), - title: this.plugin.title, - viewType: targetViewOptions.viewType, - showTitles: targetViewOptions.showTitles, - showFilter: targetViewOptions.showFilter, - isLoading: loading, - showBreadcrumbs: targetViewOptions.showBreadcrumbs, - pluginIcon: this.plugin.icon, - i18n, - validateRestrictions: this.validateRestrictions, - } - if (isInputMode) { return ( @@ -253,8 +222,29 @@ export default class SearchProviderView< return ( - {/* eslint-disable-next-line react/jsx-props-no-spreading */} - + item.type !== 'root' && item.parentId === currentFolderId) as (PartialTreeFolderNode | PartialTreeFile)[]} + nOfSelectedFiles={getNOfSelectedFiles(partialTree)} + handleScroll={this.handleScroll} + done={this.donePicking} + cancel={this.cancelPicking} + getFolder={() => {}} + showSearchFilter={targetViewOptions.showFilter} + search={this.search} + clearSearch={this.clearSearch} + searchString={searchString} + searchOnInput={false} + searchInputLabel={i18n('search')} + clearSearchLabel={i18n('resetSearch')} + noResultsLabel={i18n('noSearchResults')} + viewType={targetViewOptions.viewType} + showTitles={targetViewOptions.showTitles} + isLoading={loading} + showBreadcrumbs={targetViewOptions.showBreadcrumbs} + i18n={i18n} + validateRestrictions={this.validateRestrictions} + /> ) } From 8829a124f25a7624b2f91665b352444a5a7f802a Mon Sep 17 00:00:00 2001 From: Evgenia Karunus Date: Fri, 19 Apr 2024 21:06:19 +0400 Subject: [PATCH 064/170] - make the form controlled, hugely simplifies everything --- packages/@uppy/provider-views/src/Browser.tsx | 31 ++++--- .../src/ProviderView/ProviderView.tsx | 12 +-- .../provider-views/src/SearchFilterInput.tsx | 84 ++++++------------- .../SearchProviderView/SearchProviderView.tsx | 53 ++++++------ 4 files changed, 74 insertions(+), 106 deletions(-) diff --git a/packages/@uppy/provider-views/src/Browser.tsx b/packages/@uppy/provider-views/src/Browser.tsx index 62c5ddb8dd..6b246e1d29 100644 --- a/packages/@uppy/provider-views/src/Browser.tsx +++ b/packages/@uppy/provider-views/src/Browser.tsx @@ -30,8 +30,8 @@ type BrowserProps = { showSearchFilter: boolean search: (query: string) => void searchString: string - clearSearch: () => void - searchOnInput: boolean + setSearchString: (s: string) => void + submitSearchString: () => void searchInputLabel: string clearSearchLabel: string getFolder: (folderId: any) => void @@ -57,10 +57,11 @@ function Browser( validateRestrictions, isLoading, showSearchFilter, - search, + searchString, - clearSearch, - searchOnInput, + setSearchString, + submitSearchString, + searchInputLabel, clearSearchLabel, getFolder, @@ -91,17 +92,15 @@ function Browser( )} {showSearchFilter && ( -
            - -
            + )} {(() => { diff --git a/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx b/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx index 1bd6a6020c..42eeba53ca 100644 --- a/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx +++ b/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx @@ -371,12 +371,14 @@ export default class ProviderView extends View< // For SearchFilterInput component showSearchFilter={targetViewOptions.showFilter} - search={(input: string | undefined) => this.plugin.setPluginState({ searchString: input })} - clearSearch={() => this.plugin.setPluginState({ searchString: '' })} - searchString={searchString} - searchOnInput={true} searchInputLabel={i18n('filter')} clearSearchLabel={i18n('resetFilter')} + searchString={searchString} + setSearchString={(searchString: string) => { + console.log('setting searchString!', searchString); + this.plugin.setPluginState({ searchString }) + }} + submitSearchString={() => {}} noResultsLabel={i18n('noFilesFound')} handleScroll={this.handleScroll} @@ -386,7 +388,7 @@ export default class ProviderView extends View< showBreadcrumbs={targetViewOptions.showBreadcrumbs} getFolder={this.getFolder} - breadcrumbs={this.getBreadcrumbs} + breadcrumbs={this.getBreadcrumbs()} pluginIcon={pluginIcon} title={this.plugin.title} logout={this.logout} diff --git a/packages/@uppy/provider-views/src/SearchFilterInput.tsx b/packages/@uppy/provider-views/src/SearchFilterInput.tsx index 06f588a2ea..5070582502 100644 --- a/packages/@uppy/provider-views/src/SearchFilterInput.tsx +++ b/packages/@uppy/provider-views/src/SearchFilterInput.tsx @@ -1,89 +1,57 @@ -/* eslint-disable react/require-default-props */ -import { h, Fragment } from 'preact' -import { useEffect, useState, useCallback } from 'preact/hooks' -import { nanoid } from 'nanoid/non-secure' +import { h } from 'preact' +import type { ChangeEvent } from 'preact/compat' type Props = { - search: (query: string) => void - searchOnInput?: boolean - searchTerm?: string | null + searchString: string + setSearchString: (s: string) => void + submitSearchString: () => void + showButton?: boolean inputLabel: string clearSearchLabel?: string buttonLabel?: string - // eslint-disable-next-line react/require-default-props - clearSearch?: () => void + wrapperClassName: string inputClassName: string buttonCSSClassName?: string } export default function SearchFilterInput(props: Props): JSX.Element { const { - search, - searchOnInput, - searchTerm, + searchString, + setSearchString, + submitSearchString, + showButton, inputLabel, clearSearchLabel, buttonLabel, - clearSearch, + wrapperClassName, inputClassName, buttonCSSClassName, } = props - const [searchText, setSearchText] = useState(searchTerm ?? '') - // const debouncedSearch = debounce((q) => search(q), 1000) - - const validateAndSearch = useCallback( - (ev: Event) => { - ev.preventDefault() - search(searchText) - }, - [search, searchText], - ) - const handleInput = useCallback( - (ev: Event) => { - const inputValue = (ev.target as HTMLInputElement).value - setSearchText(inputValue) - if (searchOnInput) search(inputValue) - }, - [setSearchText, searchOnInput, search], - ) - - const handleReset = () => { - setSearchText('') - if (clearSearch) clearSearch() + const onSubmit = (e: Event) => { + e.preventDefault() + submitSearchString() } - const [form] = useState(() => { - const formEl = document.createElement('form') - formEl.setAttribute('tabindex', '-1') - formEl.id = nanoid() - return formEl - }) - - useEffect(() => { - document.body.appendChild(form) - form.addEventListener('submit', validateAndSearch) - return () => { - form.removeEventListener('submit', validateAndSearch) - document.body.removeChild(form) - } - }, [form, validateAndSearch]) + const onInput = (e: ChangeEvent) => { + setSearchString((e.target as HTMLInputElement).value) + } return ( - +
            {!showButton && ( + // 🔍 )} - {!showButton && searchText && ( + {!showButton && searchString && ( + // ❌ )} - +
            ) } diff --git a/packages/@uppy/provider-views/src/SearchProviderView/SearchProviderView.tsx b/packages/@uppy/provider-views/src/SearchProviderView/SearchProviderView.tsx index c2332b23a7..accb5c347e 100644 --- a/packages/@uppy/provider-views/src/SearchProviderView/SearchProviderView.tsx +++ b/packages/@uppy/provider-views/src/SearchProviderView/SearchProviderView.tsx @@ -67,8 +67,8 @@ export default class SearchProviderView< ) { super(plugin, { ...defaultOptions, ...opts }) + this.setSearchString = this.setSearchString.bind(this) this.search = this.search.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) @@ -89,8 +89,9 @@ export default class SearchProviderView< this.plugin.setPluginState(defaultState) } - async search(searchString: string): Promise { - this.plugin.setPluginState({ searchString }) + async search(): Promise { + const { searchString } = this.plugin.getPluginState() + if (searchString === '') return this.setLoading(true) try { @@ -121,14 +122,6 @@ export default class SearchProviderView< this.setLoading(false) } - clearSearch(): void { - this.plugin.setPluginState({ - partialTree: [], - currentFolderId: null, - searchString: '', - }) - } - async handleScroll(event: Event): Promise { const { partialTree, searchString } = this.plugin.getPluginState() const root = partialTree.find((i) => i.type === 'root') as PartialTreeFolderRoot @@ -191,31 +184,37 @@ export default class SearchProviderView< this.lastCheckbox = ourItem.id! } + setSearchString = (searchString: string) => { + this.plugin.setPluginState({ searchString }) + if (searchString === '') { + this.plugin.setPluginState({ partialTree: [] }) + } + } + render( state: unknown, viewOptions: Omit, 'provider'> = {}, ): JSX.Element { - const { isInputMode, searchString } = + const { isInputMode, searchString, loading, partialTree, currentFolderId } = this.plugin.getPluginState() const { i18n } = this.plugin.uppy - const targetViewOptions = { ...this.opts, ...viewOptions } - const { loading, partialTree, currentFolderId } = - this.plugin.getPluginState() if (isInputMode) { return ( -
            - -
            +
            ) } @@ -232,9 +231,9 @@ export default class SearchProviderView< getFolder={() => {}} showSearchFilter={targetViewOptions.showFilter} search={this.search} - clearSearch={this.clearSearch} searchString={searchString} - searchOnInput={false} + setSearchString={this.setSearchString} + submitSearchString={this.search} searchInputLabel={i18n('search')} clearSearchLabel={i18n('resetSearch')} noResultsLabel={i18n('noSearchResults')} From 9b29db110e0696ff2f5d74cb50959609bb46d9a2 Mon Sep 17 00:00:00 2001 From: Evgenia Karunus Date: Fri, 19 Apr 2024 21:23:18 +0400 Subject: [PATCH 065/170] `filterItems.ts` - move to , because it's only used there --- packages/@uppy/provider-views/src/Browser.tsx | 1 - .../src/ProviderView/ProviderView.tsx | 23 +++++++++++-------- .../provider-views/src/utils/filterItems.ts | 13 ----------- 3 files changed, 13 insertions(+), 24 deletions(-) delete mode 100644 packages/@uppy/provider-views/src/utils/filterItems.ts diff --git a/packages/@uppy/provider-views/src/Browser.tsx b/packages/@uppy/provider-views/src/Browser.tsx index 6b246e1d29..e21f98c23c 100644 --- a/packages/@uppy/provider-views/src/Browser.tsx +++ b/packages/@uppy/provider-views/src/Browser.tsx @@ -28,7 +28,6 @@ type BrowserProps = { validateRestrictions: (file: CompanionFile) => RestrictionError | null isLoading: boolean | string showSearchFilter: boolean - search: (query: string) => void searchString: string setSearchString: (s: string) => void submitSearchString: () => void diff --git a/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx b/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx index 42eeba53ca..32146a5811 100644 --- a/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx +++ b/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx @@ -24,7 +24,6 @@ import packageJson from '../../package.json' import PartialTreeUtils from '../utils/PartialTreeUtils.ts' import fillPartialTree from '../utils/fillPartialTree.ts' import getTagFile from '../utils/getTagFile.ts' -import filterItems from '../utils/filterItems.ts' import getNOfSelectedFiles from '../utils/getNOfSelectedFiles.ts' export function defaultPickerIcon(): JSX.Element { @@ -318,16 +317,23 @@ export default class ProviderView extends View< // (https://stackoverflow.com/a/1527797/3192470) document.getSelection()?.removeAllRanges() - const { partialTree, currentFolderId, searchString } = this.plugin.getPluginState() - - const itemsInThisFolder = partialTree.filter((item) => item.type !== 'root' && item.parentId === currentFolderId) - const displayedPartialTree = filterItems(itemsInThisFolder, searchString) as (PartialTreeFile | PartialTreeFolderNode)[] - const newPartialTree = PartialTreeUtils.afterToggleCheckbox(partialTree, displayedPartialTree, ourItem, this.validateRestrictions, this.isShiftKeyPressed, this.lastCheckbox) + const { partialTree } = this.plugin.getPluginState() + const newPartialTree = PartialTreeUtils.afterToggleCheckbox(partialTree, this.getDisplayedPartialTree(), ourItem, this.validateRestrictions, this.isShiftKeyPressed, this.lastCheckbox) this.plugin.setPluginState({ partialTree: newPartialTree }) this.lastCheckbox = ourItem.id! } + getDisplayedPartialTree = () : (PartialTreeFile | PartialTreeFolderNode)[] => { + const { partialTree, currentFolderId, searchString } = this.plugin.getPluginState() + const inThisFolder = partialTree.filter((item) => item.type !== 'root' && item.parentId === currentFolderId) as (PartialTreeFile | PartialTreeFolderNode)[] + const filtered = searchString === '' + ? inThisFolder + : inThisFolder.filter((item) => item.data.name.toLowerCase().indexOf(searchString.toLowerCase()) !== -1) + + return filtered + } + render( state: unknown, viewOptions: Omit, 'provider'> = {}, @@ -346,9 +352,6 @@ export default class ProviderView extends View< this.plugin.getPluginState() const pluginIcon = this.plugin.icon || defaultPickerIcon - const itemsInThisFolder = partialTree.filter((item) => item.type !== 'root' && item.parentId === currentFolderId) - const displayedPartialTree = filterItems(itemsInThisFolder, searchString) as (PartialTreeFile | PartialTreeFolderNode)[] - if (authenticated === false) { return ( extends View< return toggleCheckbox={this.toggleCheckbox.bind(this)} - displayedPartialTree={displayedPartialTree} + displayedPartialTree={this.getDisplayedPartialTree()} nOfSelectedFiles={getNOfSelectedFiles(partialTree)} getFolder={this.getFolder} loadAllFiles={this.opts.loadAllFiles} diff --git a/packages/@uppy/provider-views/src/utils/filterItems.ts b/packages/@uppy/provider-views/src/utils/filterItems.ts deleted file mode 100644 index 17c36ff5a5..0000000000 --- a/packages/@uppy/provider-views/src/utils/filterItems.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type { PartialTree } from "@uppy/core/lib/Uppy" - -const filterItems = (items: PartialTree, filterInput: string | undefined): PartialTree => { - if (!filterInput || filterInput === '') { - return items - } - return items.filter((item) => - item.type !== 'root' && - item.data.name.toLowerCase().indexOf(filterInput.toLowerCase()) !== -1 - ) -} - -export default filterItems From d493b322ab6b90bcaaeb73b192fbfe8239201091 Mon Sep 17 00:00:00 2001 From: Evgenia Karunus Date: Fri, 19 Apr 2024 22:09:14 +0400 Subject: [PATCH 066/170] /utils/PartialTreeUtils.ts - put every util in a separate file --- .../src/ProviderView/ProviderView.tsx | 5 +- .../SearchProviderView/SearchProviderView.tsx | 3 +- .../PartialTreeUtils/afterClickOnFolder.ts | 55 +++++++++++ .../src/utils/PartialTreeUtils/afterScroll.ts | 50 ++++++++++ .../afterToggleCheckbox.ts} | 99 +------------------ .../fill.ts} | 5 +- .../src/utils/PartialTreeUtils/index.ts | 11 +++ 7 files changed, 122 insertions(+), 106 deletions(-) create mode 100644 packages/@uppy/provider-views/src/utils/PartialTreeUtils/afterClickOnFolder.ts create mode 100644 packages/@uppy/provider-views/src/utils/PartialTreeUtils/afterScroll.ts rename packages/@uppy/provider-views/src/utils/{PartialTreeUtils.ts => PartialTreeUtils/afterToggleCheckbox.ts} (53%) rename packages/@uppy/provider-views/src/utils/{fillPartialTree.ts => PartialTreeUtils/fill.ts} (95%) create mode 100644 packages/@uppy/provider-views/src/utils/PartialTreeUtils/index.ts diff --git a/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx b/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx index 32146a5811..448bf7570b 100644 --- a/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx +++ b/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx @@ -21,8 +21,7 @@ import View, { type ViewOptions } from '../View.ts' // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore We don't want TS to generate types for the package.json import packageJson from '../../package.json' -import PartialTreeUtils from '../utils/PartialTreeUtils.ts' -import fillPartialTree from '../utils/fillPartialTree.ts' +import PartialTreeUtils from '../utils/PartialTreeUtils' import getTagFile from '../utils/getTagFile.ts' import getNOfSelectedFiles from '../utils/getNOfSelectedFiles.ts' @@ -257,7 +256,7 @@ export default class ProviderView extends View< this.setLoading(true) await this.#withAbort(async (signal) => { - const uppyFiles: CompanionFile[] = await fillPartialTree(partialTree, this.provider, signal) + const uppyFiles: CompanionFile[] = await PartialTreeUtils.fill(partialTree, this.provider, signal) const filesToAdd : TagFile[] = [] const filesAlreadyAdded : TagFile[] = [] diff --git a/packages/@uppy/provider-views/src/SearchProviderView/SearchProviderView.tsx b/packages/@uppy/provider-views/src/SearchProviderView/SearchProviderView.tsx index accb5c347e..ab4d3c984c 100644 --- a/packages/@uppy/provider-views/src/SearchProviderView/SearchProviderView.tsx +++ b/packages/@uppy/provider-views/src/SearchProviderView/SearchProviderView.tsx @@ -14,7 +14,7 @@ import View, { type ViewOptions } from '../View.ts' import packageJson from '../../package.json' import getTagFile from '../utils/getTagFile.ts' import getNOfSelectedFiles from '../utils/getNOfSelectedFiles.ts' -import PartialTreeUtils from '../utils/PartialTreeUtils.ts' +import PartialTreeUtils from '../utils/PartialTreeUtils' const defaultState : Partial = { isInputMode: true, @@ -230,7 +230,6 @@ export default class SearchProviderView< cancel={this.cancelPicking} getFolder={() => {}} showSearchFilter={targetViewOptions.showFilter} - search={this.search} searchString={searchString} setSearchString={this.setSearchString} submitSearchString={this.search} diff --git a/packages/@uppy/provider-views/src/utils/PartialTreeUtils/afterClickOnFolder.ts b/packages/@uppy/provider-views/src/utils/PartialTreeUtils/afterClickOnFolder.ts new file mode 100644 index 0000000000..2b6f8f021e --- /dev/null +++ b/packages/@uppy/provider-views/src/utils/PartialTreeUtils/afterClickOnFolder.ts @@ -0,0 +1,55 @@ +import type { PartialTree, PartialTreeFile, PartialTreeFolder, PartialTreeFolderNode } from "@uppy/core/lib/Uppy" +import type { CompanionFile } from "@uppy/utils/lib/CompanionFile" + +const afterClickOnFolder = ( + oldPartialTree: PartialTree, + currentItems: CompanionFile[], + clickedFolder: PartialTreeFolder, + validateRestrictions: (file: CompanionFile) => object | null, + currentPagePath: string | null +) : PartialTree => { + let newFolders = currentItems.filter((i) => i.isFolder === true) + let newFiles = currentItems.filter((i) => i.isFolder === false) + + const newlyAddedItemStatus = (clickedFolder.type === 'folder' && clickedFolder.status === 'checked') ? 'checked' : 'unchecked'; + const folders : PartialTreeFolderNode[] = newFolders.map((folder) => ({ + type: 'folder', + id: folder.requestPath, + + cached: false, + nextPagePath: null, + + status: newlyAddedItemStatus, + parentId: clickedFolder.id, + data: folder, + })) + const files : PartialTreeFile[] = newFiles.map((file) => ({ + type: 'file', + id: file.requestPath, + + status: newlyAddedItemStatus === 'checked' && validateRestrictions(file) ? 'unchecked' : newlyAddedItemStatus, + parentId: clickedFolder.id, + data: file, + })) + + // just doing `clickedFolder.cached = true` in a non-mutating way + const updatedClickedFolder : PartialTreeFolder = { + ...clickedFolder, + cached: true, + nextPagePath: currentPagePath + } + const partialTreeWithUpdatedClickedFolder = oldPartialTree.map((folder) => + folder.id === updatedClickedFolder.id ? + updatedClickedFolder : + folder + ) + + const newPartialTree = [ + ...partialTreeWithUpdatedClickedFolder, + ...folders, + ...files + ] + return newPartialTree +} + +export default afterClickOnFolder diff --git a/packages/@uppy/provider-views/src/utils/PartialTreeUtils/afterScroll.ts b/packages/@uppy/provider-views/src/utils/PartialTreeUtils/afterScroll.ts new file mode 100644 index 0000000000..cfeda7200f --- /dev/null +++ b/packages/@uppy/provider-views/src/utils/PartialTreeUtils/afterScroll.ts @@ -0,0 +1,50 @@ +import type { PartialTree, PartialTreeFile, PartialTreeFolder, PartialTreeFolderNode } from "@uppy/core/lib/Uppy" +import type { CompanionFile } from "@uppy/utils/lib/CompanionFile" + +const afterScroll = ( + oldPartialTree: PartialTree, + currentFolderId: string | null, + items: CompanionFile[], + nextPagePath: string | null, + validateRestrictions: (file: CompanionFile) => object | null, +) : PartialTree => { + const currentFolder = oldPartialTree.find((i) => i.id === currentFolderId) as PartialTreeFolder + + let newFolders = items.filter((i) => i.isFolder === true) + let newFiles = items.filter((i) => i.isFolder === false) + + // just doing `scrolledFolder.nextPagePath = ...` in a non-mutating way + const scrolledFolder : PartialTreeFolder = { ...currentFolder, nextPagePath } + const partialTreeWithUpdatedScrolledFolder = oldPartialTree.map((folder) => + folder.id === scrolledFolder.id ? scrolledFolder : folder + ) + const newlyAddedItemStatus = (scrolledFolder.type === 'folder' && scrolledFolder.status === 'checked') ? 'checked' : 'unchecked'; + const folders : PartialTreeFolderNode[] = newFolders.map((folder) => ({ + type: 'folder', + id: folder.requestPath, + + cached: false, + nextPagePath: null, + + status: newlyAddedItemStatus, + parentId: scrolledFolder.id, + data: folder, + })) + const files : PartialTreeFile[] = newFiles.map((file) => ({ + type: 'file', + id: file.requestPath, + + status: newlyAddedItemStatus === 'checked' && validateRestrictions(file) ? 'unchecked' : newlyAddedItemStatus, + parentId: scrolledFolder.id, + data: file, + })) + + const newPartialTree : PartialTree = [ + ...partialTreeWithUpdatedScrolledFolder, + ...folders, + ...files + ] + return newPartialTree +} + +export default afterScroll diff --git a/packages/@uppy/provider-views/src/utils/PartialTreeUtils.ts b/packages/@uppy/provider-views/src/utils/PartialTreeUtils/afterToggleCheckbox.ts similarity index 53% rename from packages/@uppy/provider-views/src/utils/PartialTreeUtils.ts rename to packages/@uppy/provider-views/src/utils/PartialTreeUtils/afterToggleCheckbox.ts index 6473f0948b..decbdcf12e 100644 --- a/packages/@uppy/provider-views/src/utils/PartialTreeUtils.ts +++ b/packages/@uppy/provider-views/src/utils/PartialTreeUtils/afterToggleCheckbox.ts @@ -82,101 +82,4 @@ const afterToggleCheckbox = ( return newPartialTree } -const afterClickOnFolder = ( - oldPartialTree: PartialTree, - currentItems: CompanionFile[], - clickedFolder: PartialTreeFolder, - validateRestrictions: (file: CompanionFile) => object | null, - currentPagePath: string | null -) : PartialTree => { - let newFolders = currentItems.filter((i) => i.isFolder === true) - let newFiles = currentItems.filter((i) => i.isFolder === false) - - const newlyAddedItemStatus = (clickedFolder.type === 'folder' && clickedFolder.status === 'checked') ? 'checked' : 'unchecked'; - const folders : PartialTreeFolderNode[] = newFolders.map((folder) => ({ - type: 'folder', - id: folder.requestPath, - - cached: false, - nextPagePath: null, - - status: newlyAddedItemStatus, - parentId: clickedFolder.id, - data: folder, - })) - const files : PartialTreeFile[] = newFiles.map((file) => ({ - type: 'file', - id: file.requestPath, - - status: newlyAddedItemStatus === 'checked' && validateRestrictions(file) ? 'unchecked' : newlyAddedItemStatus, - parentId: clickedFolder.id, - data: file, - })) - - // just doing `clickedFolder.cached = true` in a non-mutating way - const updatedClickedFolder : PartialTreeFolder = { - ...clickedFolder, - cached: true, - nextPagePath: currentPagePath - } - const partialTreeWithUpdatedClickedFolder = oldPartialTree.map((folder) => - folder.id === updatedClickedFolder.id ? - updatedClickedFolder : - folder - ) - - const newPartialTree = [ - ...partialTreeWithUpdatedClickedFolder, - ...folders, - ...files - ] - return newPartialTree -} - -const afterScroll = ( - oldPartialTree: PartialTree, - currentFolderId: string | null, - items: CompanionFile[], - nextPagePath: string | null, - validateRestrictions: (file: CompanionFile) => object | null, -) : PartialTree => { - const currentFolder = oldPartialTree.find((i) => i.id === currentFolderId) as PartialTreeFolder - - let newFolders = items.filter((i) => i.isFolder === true) - let newFiles = items.filter((i) => i.isFolder === false) - - // just doing `scrolledFolder.nextPagePath = ...` in a non-mutating way - const scrolledFolder : PartialTreeFolder = { ...currentFolder, nextPagePath } - const partialTreeWithUpdatedScrolledFolder = oldPartialTree.map((folder) => - folder.id === scrolledFolder.id ? scrolledFolder : folder - ) - const newlyAddedItemStatus = (scrolledFolder.type === 'folder' && scrolledFolder.status === 'checked') ? 'checked' : 'unchecked'; - const folders : PartialTreeFolderNode[] = newFolders.map((folder) => ({ - type: 'folder', - id: folder.requestPath, - - cached: false, - nextPagePath: null, - - status: newlyAddedItemStatus, - parentId: scrolledFolder.id, - data: folder, - })) - const files : PartialTreeFile[] = newFiles.map((file) => ({ - type: 'file', - id: file.requestPath, - - status: newlyAddedItemStatus === 'checked' && validateRestrictions(file) ? 'unchecked' : newlyAddedItemStatus, - parentId: scrolledFolder.id, - data: file, - })) - - const newPartialTree : PartialTree = [ - ...partialTreeWithUpdatedScrolledFolder, - ...folders, - ...files - ] - return newPartialTree -} - -export default { afterToggleCheckbox, afterClickOnFolder, afterScroll } +export default afterToggleCheckbox diff --git a/packages/@uppy/provider-views/src/utils/fillPartialTree.ts b/packages/@uppy/provider-views/src/utils/PartialTreeUtils/fill.ts similarity index 95% rename from packages/@uppy/provider-views/src/utils/fillPartialTree.ts rename to packages/@uppy/provider-views/src/utils/PartialTreeUtils/fill.ts index 7d9dacc3b3..7ee3840d6d 100644 --- a/packages/@uppy/provider-views/src/utils/fillPartialTree.ts +++ b/packages/@uppy/provider-views/src/utils/PartialTreeUtils/fill.ts @@ -3,7 +3,6 @@ import type { CompanionClientProvider, RequestOptions } from "@uppy/utils/lib/Co import type { CompanionFile } from "@uppy/utils/lib/CompanionFile" import PQueue from "p-queue" - const getAbsPath = (partialTree: PartialTree, file: PartialTreeFile) : (PartialTreeFile | PartialTreeFolderNode)[] => { const path : (PartialTreeFile | PartialTreeFolderNode)[] = [] let parent: PartialTreeFile | PartialTreeFolder = file @@ -68,7 +67,7 @@ const recursivelyFetch = async (queue: PQueue, poorTree: PartialTree, poorFolder return [] } -const fillPartialTree = async (partialTree: PartialTree, provider: CompanionClientProvider, signal: AbortSignal) : Promise => { +const fill = async (partialTree: PartialTree, provider: CompanionClientProvider, signal: AbortSignal) : Promise => { const queue = new PQueue({ concurrency: 6 }) // fill up the missing parts of a partialTree! @@ -105,4 +104,4 @@ const fillPartialTree = async (partialTree: PartialTree, provider: CompanionClie return uppyFiles } -export default fillPartialTree; +export default fill; diff --git a/packages/@uppy/provider-views/src/utils/PartialTreeUtils/index.ts b/packages/@uppy/provider-views/src/utils/PartialTreeUtils/index.ts new file mode 100644 index 0000000000..8749e5e88d --- /dev/null +++ b/packages/@uppy/provider-views/src/utils/PartialTreeUtils/index.ts @@ -0,0 +1,11 @@ +import afterClickOnFolder from './afterClickOnFolder' +import afterScroll from './afterScroll' +import afterToggleCheckbox from './afterToggleCheckbox' +import fill from './fill' + +export default { + afterClickOnFolder, + afterScroll, + afterToggleCheckbox, + fill +} From d1f764c4f6aadead4781b6529668dbaff0436406 Mon Sep 17 00:00:00 2001 From: Evgenia Karunus Date: Fri, 19 Apr 2024 22:28:06 +0400 Subject: [PATCH 067/170] `shouldHandleScroll.ts` - factor out into a util This brings all references to `this.isHandlingScroll` into a single place, and makes `shouldHandleScroll()` a self-contained simple function --- .../provider-views/src/ProviderView/ProviderView.tsx | 3 ++- .../src/SearchProviderView/SearchProviderView.tsx | 3 ++- packages/@uppy/provider-views/src/View.ts | 8 -------- .../@uppy/provider-views/src/utils/shouldHandleScroll.ts | 9 +++++++++ private/dev/Dashboard.js | 4 ++-- 5 files changed, 15 insertions(+), 12 deletions(-) create mode 100644 packages/@uppy/provider-views/src/utils/shouldHandleScroll.ts diff --git a/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx b/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx index 448bf7570b..0618f00db6 100644 --- a/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx +++ b/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx @@ -24,6 +24,7 @@ import packageJson from '../../package.json' import PartialTreeUtils from '../utils/PartialTreeUtils' import getTagFile from '../utils/getTagFile.ts' import getNOfSelectedFiles from '../utils/getNOfSelectedFiles.ts' +import shouldHandleScroll from '../utils/shouldHandleScroll.ts' export function defaultPickerIcon(): JSX.Element { return ( @@ -239,7 +240,7 @@ export default class ProviderView extends View< async handleScroll(event: Event): Promise { const { partialTree, currentFolderId } = this.plugin.getPluginState() const currentFolder = partialTree.find((i) => i.id === currentFolderId) as PartialTreeFolder - if (this.shouldHandleScroll(event) && currentFolder.nextPagePath) { + if (shouldHandleScroll(event) && !this.isHandlingScroll && currentFolder.nextPagePath) { this.isHandlingScroll = true await this.#withAbort(async (signal) => { const { nextPagePath, items } = await this.provider.list(currentFolder.nextPagePath!, { signal }) diff --git a/packages/@uppy/provider-views/src/SearchProviderView/SearchProviderView.tsx b/packages/@uppy/provider-views/src/SearchProviderView/SearchProviderView.tsx index ab4d3c984c..771162ca7d 100644 --- a/packages/@uppy/provider-views/src/SearchProviderView/SearchProviderView.tsx +++ b/packages/@uppy/provider-views/src/SearchProviderView/SearchProviderView.tsx @@ -15,6 +15,7 @@ import packageJson from '../../package.json' import getTagFile from '../utils/getTagFile.ts' import getNOfSelectedFiles from '../utils/getNOfSelectedFiles.ts' import PartialTreeUtils from '../utils/PartialTreeUtils' +import shouldHandleScroll from '../utils/shouldHandleScroll.ts' const defaultState : Partial = { isInputMode: true, @@ -126,7 +127,7 @@ export default class SearchProviderView< const { partialTree, searchString } = this.plugin.getPluginState() const root = partialTree.find((i) => i.type === 'root') as PartialTreeFolderRoot - if (this.shouldHandleScroll(event) && root.nextPagePath) { + if (shouldHandleScroll(event) && !this.isHandlingScroll && root.nextPagePath) { this.isHandlingScroll = true try { const response = await this.provider.search(searchString, root.nextPagePath) diff --git a/packages/@uppy/provider-views/src/View.ts b/packages/@uppy/provider-views/src/View.ts index a9a74dd60d..60bc1674b7 100644 --- a/packages/@uppy/provider-views/src/View.ts +++ b/packages/@uppy/provider-views/src/View.ts @@ -92,14 +92,6 @@ export default class View< return this.plugin.uppy.validateRestrictions(localData, [...aleadyAddedFiles, ...checkedFilesData]) } - shouldHandleScroll(event: Event): boolean { - const { scrollHeight, scrollTop, offsetHeight } = - event.target as HTMLElement - const scrollPosition = scrollHeight - (scrollTop + offsetHeight) - - return scrollPosition < 50 && !this.isHandlingScroll - } - cancelPicking(): void { const dashboard = this.plugin.uppy.getPlugin('Dashboard') diff --git a/packages/@uppy/provider-views/src/utils/shouldHandleScroll.ts b/packages/@uppy/provider-views/src/utils/shouldHandleScroll.ts new file mode 100644 index 0000000000..e8d23a3763 --- /dev/null +++ b/packages/@uppy/provider-views/src/utils/shouldHandleScroll.ts @@ -0,0 +1,9 @@ +const shouldHandleScroll = (event: Event) : boolean => { + const { scrollHeight, scrollTop, offsetHeight } = + event.target as HTMLElement + const scrollPosition = scrollHeight - (scrollTop + offsetHeight) + + return scrollPosition < 50 +} + +export default shouldHandleScroll diff --git a/private/dev/Dashboard.js b/private/dev/Dashboard.js index 6b20714a98..a97364fe4a 100644 --- a/private/dev/Dashboard.js +++ b/private/dev/Dashboard.js @@ -78,8 +78,8 @@ function getCompanionKeysParams (name) { // Rest is implementation! Obviously edit as necessary... export default () => { - const restrictions = undefined - // const restrictions = { requiredMetaFields: ['caption'], maxNumberOfFiles: 3 } + // const restrictions = undefined + const restrictions = undefined;// { maxFileSize: 1, maxNumberOfFiles: 3 } const uppyDashboard = new Uppy({ logger: debugLogger, From 51584c1e535df5da4bffbc54b2525cd09df6d4df Mon Sep 17 00:00:00 2001 From: Evgenia Karunus Date: Mon, 22 Apr 2024 22:04:41 +0400 Subject: [PATCH 068/170] this.state - make sure state is reset 1. on cancel 2. on close --- .../@uppy/provider-views/src/CloseWrapper.ts | 13 --- .../src/ProviderView/ProviderView.tsx | 22 +++-- .../SearchProviderView/SearchProviderView.tsx | 89 ++++++++++--------- packages/@uppy/provider-views/src/View.ts | 11 --- 4 files changed, 65 insertions(+), 70 deletions(-) delete mode 100644 packages/@uppy/provider-views/src/CloseWrapper.ts diff --git a/packages/@uppy/provider-views/src/CloseWrapper.ts b/packages/@uppy/provider-views/src/CloseWrapper.ts deleted file mode 100644 index 14502e9b55..0000000000 --- a/packages/@uppy/provider-views/src/CloseWrapper.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Component, toChildArray } from 'preact' - -export default class CloseWrapper extends Component<{ onUnmount: () => void }> { - componentWillUnmount(): void { - const { onUnmount } = this.props - onUnmount() - } - - render(): ReturnType[0] { - const { children } = this.props - return toChildArray(children)[0] - } -} diff --git a/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx b/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx index 0618f00db6..5362e47e70 100644 --- a/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx +++ b/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx @@ -105,24 +105,34 @@ export default class ProviderView extends View< this.handleScroll = this.handleScroll.bind(this) this.donePicking = this.donePicking.bind(this) this.render = this.render.bind(this) + this.cancelPicking = this.cancelPicking.bind(this) // Set default state for the plugin - this.plugin.setPluginState(getDefaultState(this.plugin.rootFolderId)) + this.resetPluginState() - const onClosePanel = () => { - this.plugin.setPluginState(getDefaultState(this.plugin.rootFolderId)) - } // @ts-expect-error this should be typed in @uppy/dashboard. - this.plugin.uppy.on('dashboard:close-panel', onClosePanel) - this.plugin.uppy.on('cancel-all', onClosePanel) + this.plugin.uppy.on('dashboard:close-panel', this.resetPluginState) this.registerRequestClient() } + resetPluginState(): void { + this.plugin.setPluginState(getDefaultState(this.plugin.rootFolderId)) + } + tearDown(): void { // Nothing. } + cancelPicking(): void { + const dashboard = this.plugin.uppy.getPlugin('Dashboard') + if (dashboard) { + // @ts-expect-error impossible to type this correctly without adding dashboard + // as a dependency to this package. + dashboard.hideAllPanels() + } + } + #abortController: AbortController | undefined async #withAbort(op: (signal: AbortSignal) => Promise) { diff --git a/packages/@uppy/provider-views/src/SearchProviderView/SearchProviderView.tsx b/packages/@uppy/provider-views/src/SearchProviderView/SearchProviderView.tsx index 771162ca7d..1f7a0e4ecf 100644 --- a/packages/@uppy/provider-views/src/SearchProviderView/SearchProviderView.tsx +++ b/packages/@uppy/provider-views/src/SearchProviderView/SearchProviderView.tsx @@ -6,7 +6,6 @@ import type { DefinePluginOpts } from '@uppy/core/lib/BasePlugin.ts' import type { CompanionFile } from '@uppy/utils/lib/CompanionFile' import SearchFilterInput from '../SearchFilterInput.tsx' import Browser from '../Browser.tsx' -import CloseWrapper from '../CloseWrapper.ts' import View, { type ViewOptions } from '../View.ts' // eslint-disable-next-line @typescript-eslint/ban-ts-comment @@ -73,10 +72,15 @@ export default class SearchProviderView< this.resetPluginState = this.resetPluginState.bind(this) this.handleScroll = this.handleScroll.bind(this) this.donePicking = this.donePicking.bind(this) + this.cancelPicking = this.cancelPicking.bind(this) this.render = this.render.bind(this) - this.plugin.setPluginState(defaultState) + // Set default state for the plugin + this.resetPluginState() + + // @ts-expect-error this should be typed in @uppy/dashboard. + this.plugin.uppy.on('dashboard:close-panel', this.resetPluginState) this.registerRequestClient() } @@ -90,6 +94,15 @@ export default class SearchProviderView< this.plugin.setPluginState(defaultState) } + cancelPicking(): void { + const dashboard = this.plugin.uppy.getPlugin('Dashboard') + if (dashboard) { + // @ts-expect-error impossible to type this correctly without adding dashboard + // as a dependency to this package. + dashboard.hideAllPanels() + } + } + async search(): Promise { const { searchString } = this.plugin.getPluginState() if (searchString === '') return @@ -203,48 +216,44 @@ export default class SearchProviderView< if (isInputMode) { return ( - - - - ) - } - - return ( - - item.type !== 'root' && item.parentId === currentFolderId) as (PartialTreeFolderNode | PartialTreeFile)[]} - nOfSelectedFiles={getNOfSelectedFiles(partialTree)} - handleScroll={this.handleScroll} - done={this.donePicking} - cancel={this.cancelPicking} - getFolder={() => {}} - showSearchFilter={targetViewOptions.showFilter} + - + ) + } + + return ( + item.type !== 'root' && item.parentId === currentFolderId) as (PartialTreeFolderNode | PartialTreeFile)[]} + nOfSelectedFiles={getNOfSelectedFiles(partialTree)} + handleScroll={this.handleScroll} + done={this.donePicking} + cancel={this.cancelPicking} + getFolder={() => {}} + showSearchFilter={targetViewOptions.showFilter} + searchString={searchString} + setSearchString={this.setSearchString} + submitSearchString={this.search} + searchInputLabel={i18n('search')} + clearSearchLabel={i18n('resetSearch')} + noResultsLabel={i18n('noSearchResults')} + viewType={targetViewOptions.viewType} + showTitles={targetViewOptions.showTitles} + isLoading={loading} + showBreadcrumbs={targetViewOptions.showBreadcrumbs} + i18n={i18n} + validateRestrictions={this.validateRestrictions} + /> ) } } diff --git a/packages/@uppy/provider-views/src/View.ts b/packages/@uppy/provider-views/src/View.ts index 60bc1674b7..937626c943 100644 --- a/packages/@uppy/provider-views/src/View.ts +++ b/packages/@uppy/provider-views/src/View.ts @@ -62,7 +62,6 @@ export default class View< this.isHandlingScroll = false this.handleError = this.handleError.bind(this) - this.cancelPicking = this.cancelPicking.bind(this) this.validateRestrictions = this.validateRestrictions.bind(this) // This records whether the user is holding the SHIFT key this very moment. @@ -92,16 +91,6 @@ export default class View< return this.plugin.uppy.validateRestrictions(localData, [...aleadyAddedFiles, ...checkedFilesData]) } - cancelPicking(): void { - const dashboard = this.plugin.uppy.getPlugin('Dashboard') - - if (dashboard) { - // @ts-expect-error impossible to type this correctly without adding dashboard - // as a dependency to this package. - dashboard.hideAllPanels() - } - } - handleError(error: Error): void { const { uppy } = this.plugin // authError just means we're not authenticated, don't report it From decc4cbd7c4eccc80a733263d1f87d053c11e374 Mon Sep 17 00:00:00 2001 From: Evgenia Karunus Date: Mon, 22 Apr 2024 22:11:01 +0400 Subject: [PATCH 069/170] `this.xxx` - never leave `this.xxx` variables undefined --- .../@uppy/provider-views/src/ProviderView/ProviderView.tsx | 2 +- packages/@uppy/provider-views/src/View.ts | 4 +++- .../src/utils/PartialTreeUtils/afterToggleCheckbox.ts | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx b/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx index 5362e47e70..9582bcd4d3 100644 --- a/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx +++ b/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx @@ -167,7 +167,7 @@ export default class ProviderView extends View< * */ async getFolder(folderId: string | null): Promise { - this.lastCheckbox = undefined + this.lastCheckbox = null console.log(`____________________________________________GETTING FOLDER "${folderId}"`); // Returning cached folder const { partialTree } = this.plugin.getPluginState() diff --git a/packages/@uppy/provider-views/src/View.ts b/packages/@uppy/provider-views/src/View.ts index 937626c943..ee3ac6dadb 100644 --- a/packages/@uppy/provider-views/src/View.ts +++ b/packages/@uppy/provider-views/src/View.ts @@ -50,7 +50,7 @@ export default class View< isShiftKeyPressed: boolean - lastCheckbox: string | undefined + lastCheckbox: string | null protected opts: O @@ -60,6 +60,8 @@ export default class View< this.opts = opts this.isHandlingScroll = false + this.isShiftKeyPressed = false + this.lastCheckbox = null this.handleError = this.handleError.bind(this) this.validateRestrictions = this.validateRestrictions.bind(this) diff --git a/packages/@uppy/provider-views/src/utils/PartialTreeUtils/afterToggleCheckbox.ts b/packages/@uppy/provider-views/src/utils/PartialTreeUtils/afterToggleCheckbox.ts index decbdcf12e..681c6155db 100644 --- a/packages/@uppy/provider-views/src/utils/PartialTreeUtils/afterToggleCheckbox.ts +++ b/packages/@uppy/provider-views/src/utils/PartialTreeUtils/afterToggleCheckbox.ts @@ -7,7 +7,7 @@ const afterToggleCheckbox = ( ourItem: PartialTreeFolderNode | PartialTreeFile, validateRestrictions: (file: CompanionFile) => object | null, isShiftKeyPressed: boolean, - lastCheckbox: string | undefined + lastCheckbox: string | null ) : PartialTree => { const newPartialTree : PartialTree = JSON.parse(JSON.stringify(oldPartialTree)) From 6db223e2aa8c79d07377a2cdb977f1204e64b0b6 Mon Sep 17 00:00:00 2001 From: Evgenia Karunus Date: Mon, 22 Apr 2024 22:26:43 +0400 Subject: [PATCH 070/170] `this.username` - should be in `this.state` Also - when there is no username, stop showing the little dot --- packages/@uppy/core/src/Uppy.ts | 1 + .../provider-views/src/ProviderView/Header.tsx | 6 +++--- .../src/ProviderView/ProviderView.tsx | 14 +++++++------- .../@uppy/provider-views/src/ProviderView/User.tsx | 11 +++++++---- 4 files changed, 18 insertions(+), 14 deletions(-) diff --git a/packages/@uppy/core/src/Uppy.ts b/packages/@uppy/core/src/Uppy.ts index 782fb799aa..d573e0b307 100644 --- a/packages/@uppy/core/src/Uppy.ts +++ b/packages/@uppy/core/src/Uppy.ts @@ -114,6 +114,7 @@ export type UnknownProviderPluginState = { loading: boolean | string partialTree: PartialTree currentFolderId: string | null + username: string | null } /* * UnknownProviderPlugin can be any Companion plugin (such as Google Drive). diff --git a/packages/@uppy/provider-views/src/ProviderView/Header.tsx b/packages/@uppy/provider-views/src/ProviderView/Header.tsx index f0e3d70dfa..fe7e37ea36 100644 --- a/packages/@uppy/provider-views/src/ProviderView/Header.tsx +++ b/packages/@uppy/provider-views/src/ProviderView/Header.tsx @@ -2,7 +2,7 @@ import { h, Fragment } from 'preact' import type { I18n } from '@uppy/utils/lib/Translator' import type { Body, Meta } from '@uppy/utils/lib/UppyFile' -import type { UnknownProviderPluginState } from '@uppy/core/lib/Uppy.ts' +import type { PartialTreeFolder } from '@uppy/core/lib/Uppy.ts' import User from './User.tsx' import Breadcrumbs from '../Breadcrumbs.tsx' import type ProviderView from './ProviderView.tsx' @@ -10,11 +10,11 @@ import type ProviderView from './ProviderView.tsx' type HeaderProps = { showBreadcrumbs: boolean getFolder: ProviderView['getFolder'] - breadcrumbs: UnknownProviderPluginState['breadcrumbs'] + breadcrumbs: PartialTreeFolder[] pluginIcon: () => JSX.Element title: string logout: () => void - username: string | undefined + username: string | null i18n: I18n } diff --git a/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx b/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx index 9582bcd4d3..64081a124c 100644 --- a/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx +++ b/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx @@ -65,7 +65,7 @@ type Opts = DefinePluginOpts< keyof typeof defaultOptions > -const getDefaultState = (rootFolderId: string | null) : Partial => ({ +const getDefaultState = (rootFolderId: string | null) : UnknownProviderPluginState => ({ authenticated: undefined, // we don't know yet partialTree: [ { @@ -77,7 +77,9 @@ const getDefaultState = (rootFolderId: string | null) : Partial extends View< > { static VERSION = packageJson.version - username: string | undefined - constructor( plugin: UnknownProviderPlugin, opts: ProviderViewOptions, @@ -185,7 +185,7 @@ export default class ProviderView extends View< do { const { username, nextPagePath, items } = await this.provider.list(currentPagePath, { signal }) // It's important to set the username during one of our first fetches - this.username = username + this.plugin.setPluginState({ username }) currentPagePath = nextPagePath currentItems = currentItems.concat(items) @@ -358,7 +358,7 @@ export default class ProviderView extends View< } const targetViewOptions = { ...this.opts, ...viewOptions } - const { partialTree, currentFolderId, searchString, loading } = + const { partialTree, username, searchString, loading } = this.plugin.getPluginState() const pluginIcon = this.plugin.icon || defaultPickerIcon @@ -405,7 +405,7 @@ export default class ProviderView extends View< pluginIcon={pluginIcon} title={this.plugin.title} logout={this.logout} - username={this.username} + username={username} i18n={i18n} /> } diff --git a/packages/@uppy/provider-views/src/ProviderView/User.tsx b/packages/@uppy/provider-views/src/ProviderView/User.tsx index 9db5c55a43..552a0ed393 100644 --- a/packages/@uppy/provider-views/src/ProviderView/User.tsx +++ b/packages/@uppy/provider-views/src/ProviderView/User.tsx @@ -3,7 +3,7 @@ import { h, Fragment } from 'preact' type UserProps = { i18n: (phrase: string) => string logout: () => void - username: string | undefined + username: string | null } export default function User({ @@ -13,9 +13,12 @@ export default function User({ }: UserProps): JSX.Element { return ( - - {username} - + { + username && + + {username} + + } diff --git a/packages/@uppy/provider-views/src/Browser.tsx b/packages/@uppy/provider-views/src/Browser.tsx index a92cd4426f..0da8647805 100644 --- a/packages/@uppy/provider-views/src/Browser.tsx +++ b/packages/@uppy/provider-views/src/Browser.tsx @@ -33,7 +33,7 @@ type BrowserProps = { submitSearchString: () => void searchInputLabel: string clearSearchLabel: string - getFolder: (folderId: any) => void + openFolder: (folderId: any) => void cancel: () => void done: () => void noResultsLabel: string @@ -62,7 +62,7 @@ function Browser( searchInputLabel, clearSearchLabel, - getFolder, + openFolder, cancel, done, noResultsLabel, @@ -135,7 +135,7 @@ function Browser( showTitles={showTitles} i18n={i18n} validateRestrictions={validateRestrictions} - getFolder={getFolder} + openFolder={openFolder} file={file} /> )} @@ -162,7 +162,7 @@ function Browser( showTitles={showTitles} i18n={i18n} validateRestrictions={validateRestrictions} - getFolder={getFolder} + openFolder={openFolder} file={file} /> ))} diff --git a/packages/@uppy/provider-views/src/Item/index.tsx b/packages/@uppy/provider-views/src/Item/index.tsx index 65f246ea28..6faf056b77 100644 --- a/packages/@uppy/provider-views/src/Item/index.tsx +++ b/packages/@uppy/provider-views/src/Item/index.tsx @@ -19,14 +19,14 @@ type ItemProps = { showTitles: boolean i18n: I18n validateRestrictions: (file: CompanionFile) => RestrictionError | null - getFolder: (folderId: PartialTreeId) => void + openFolder: (folderId: PartialTreeId) => void file: PartialTreeFile | PartialTreeFolderNode } export default function Item( props: ItemProps, ): h.JSX.Element { - const { viewType, toggleCheckbox, showTitles, i18n, validateRestrictions, getFolder, file } = props + const { viewType, toggleCheckbox, showTitles, i18n, validateRestrictions, openFolder, file } = props const restrictionError = validateRestrictions(file.data) const isDisabled = file.data.isFolder ? false : (Boolean(restrictionError) && (file.status !== "checked")) @@ -57,7 +57,7 @@ export default function Item( ...sharedProps, type: 'folder', isCheckboxDisabled: file.id === VIRTUAL_SHARED_DIR, - handleFolderClick: () => getFolder(file.id), + handleFolderClick: () => openFolder(file.id), } : { ...sharedProps, diff --git a/packages/@uppy/provider-views/src/ProviderView/Header.tsx b/packages/@uppy/provider-views/src/ProviderView/Header.tsx index a4d4f343a6..1773aff38c 100644 --- a/packages/@uppy/provider-views/src/ProviderView/Header.tsx +++ b/packages/@uppy/provider-views/src/ProviderView/Header.tsx @@ -10,7 +10,7 @@ import classNames from 'classnames' type HeaderProps = { showBreadcrumbs: boolean - getFolder: ProviderView['getFolder'] + openFolder: ProviderView['openFolder'] breadcrumbs: PartialTreeFolder[] pluginIcon: () => JSX.Element title: string @@ -32,7 +32,7 @@ export default function Header( > {props.showBreadcrumbs && ( { } this.opts = { ...defaultOptions, ...opts } - this.getFolder = this.getFolder.bind(this) + this.openFolder = this.openFolder.bind(this) this.logout = this.logout.bind(this) this.handleAuth = this.handleAuth.bind(this) this.handleScroll = this.handleScroll.bind(this) @@ -175,12 +175,7 @@ export default class ProviderView{ } } - /** - * 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?) - * - */ - async getFolder(folderId: string | null): Promise { + async openFolder(folderId: string | null): Promise { this.lastCheckbox = null console.log(`____________________________________________GETTING FOLDER "${folderId}"`); // Returning cached folder @@ -206,7 +201,7 @@ export default class ProviderView{ this.setLoading(this.plugin.uppy.i18n('loadedXFiles', { numFiles: items.length })) } while (this.opts.loadAllFiles && currentPagePath) - const newPartialTree = PartialTreeUtils.afterClickOnFolder(partialTree, currentItems, clickedFolder, validateRestrictions(this.plugin), currentPagePath) + const newPartialTree = PartialTreeUtils.afterOpenFolder(partialTree, currentItems, clickedFolder, validateRestrictions(this.plugin), currentPagePath) this.plugin.setPluginState({ partialTree: newPartialTree, @@ -255,7 +250,7 @@ export default class ProviderView{ this.plugin.setPluginState({ authenticated: true }) await Promise.all([ this.provider.fetchPreAuthToken(), - this.getFolder(this.plugin.rootFolderId), + this.openFolder(this.plugin.rootFolderId), ]) }).catch(handleError(this.plugin.uppy)) this.setLoading(false) @@ -370,7 +365,7 @@ export default class ProviderView{ if (!didFirstRender) { this.plugin.setPluginState({ didFirstRender: true }) this.provider.fetchPreAuthToken() - this.getFolder(this.plugin.rootFolderId) + this.openFolder(this.plugin.rootFolderId) } const opts : Opts = { ...this.opts, ...viewOptions } @@ -395,7 +390,7 @@ export default class ProviderView{ toggleCheckbox={this.toggleCheckbox} displayedPartialTree={this.getDisplayedPartialTree()} nOfSelectedFiles={getNOfSelectedFiles(partialTree)} - getFolder={this.getFolder} + openFolder={this.openFolder} loadAllFiles={opts.loadAllFiles} // For SearchFilterInput component @@ -416,7 +411,7 @@ export default class ProviderView{ headerComponent={ showBreadcrumbs={opts.showBreadcrumbs} - getFolder={this.getFolder} + openFolder={this.openFolder} breadcrumbs={this.getBreadcrumbs()} pluginIcon={pluginIcon} title={this.plugin.title} diff --git a/packages/@uppy/provider-views/src/SearchProviderView/SearchProviderView.tsx b/packages/@uppy/provider-views/src/SearchProviderView/SearchProviderView.tsx index 30a062eb87..87591b1be7 100644 --- a/packages/@uppy/provider-views/src/SearchProviderView/SearchProviderView.tsx +++ b/packages/@uppy/provider-views/src/SearchProviderView/SearchProviderView.tsx @@ -259,7 +259,7 @@ export default class SearchProviderView { handleScroll={this.handleScroll} done={this.donePicking} cancel={this.cancelPicking} - getFolder={() => {}} + openFolder={() => {}} showSearchFilter={opts.showFilter} searchString={searchString} setSearchString={this.setSearchString} diff --git a/packages/@uppy/provider-views/src/utils/PartialTreeUtils/afterClickOnFolder.ts b/packages/@uppy/provider-views/src/utils/PartialTreeUtils/afterOpenFolder.ts similarity index 96% rename from packages/@uppy/provider-views/src/utils/PartialTreeUtils/afterClickOnFolder.ts rename to packages/@uppy/provider-views/src/utils/PartialTreeUtils/afterOpenFolder.ts index 506a21e90f..25dcb00d23 100644 --- a/packages/@uppy/provider-views/src/utils/PartialTreeUtils/afterClickOnFolder.ts +++ b/packages/@uppy/provider-views/src/utils/PartialTreeUtils/afterOpenFolder.ts @@ -1,7 +1,7 @@ import type { PartialTree, PartialTreeFile, PartialTreeFolder, PartialTreeFolderNode } from "@uppy/core/lib/Uppy" import type { CompanionFile } from "@uppy/utils/lib/CompanionFile" -const afterClickOnFolder = ( +const afterOpenFolder = ( oldPartialTree: PartialTree, discoveredItems: CompanionFile[], clickedFolder: PartialTreeFolder, @@ -51,4 +51,4 @@ const afterClickOnFolder = ( return newPartialTree } -export default afterClickOnFolder +export default afterOpenFolder diff --git a/packages/@uppy/provider-views/src/utils/PartialTreeUtils/index.test.ts b/packages/@uppy/provider-views/src/utils/PartialTreeUtils/index.test.ts index aefcea098f..98694a4936 100644 --- a/packages/@uppy/provider-views/src/utils/PartialTreeUtils/index.test.ts +++ b/packages/@uppy/provider-views/src/utils/PartialTreeUtils/index.test.ts @@ -2,7 +2,7 @@ import { describe, expect, it } from 'vitest' import afterToggleCheckbox from './afterToggleCheckbox.ts' import type { PartialTree, PartialTreeFile, PartialTreeFolderNode, PartialTreeFolderRoot } from '@uppy/core/lib/Uppy.ts' import type { CompanionFile } from '@uppy/utils/lib/CompanionFile' -import afterClickOnFolder from './afterClickOnFolder.ts' +import afterOpenFolder from './afterOpenFolder.ts' const _root = (id: string, options: any = {}) : PartialTreeFolderRoot => ({ type: 'root', @@ -130,7 +130,7 @@ describe('afterToggleCheckbox', () => { }) }) -describe('afterClickOnFolder', () => { +describe('afterOpenFolder', () => { it('open "checked" folder - all discovered files are marked as "checked"', () => { const oldPartialTree : PartialTree = [ _root('ourRoot'), @@ -142,7 +142,7 @@ describe('afterClickOnFolder', () => { const clickedFolder = oldPartialTree.find((f) => f.id === '2') as PartialTreeFolderNode - const newTree = afterClickOnFolder(oldPartialTree, fakeCompanionFiles, clickedFolder, () => null, null) + const newTree = afterOpenFolder(oldPartialTree, fakeCompanionFiles, clickedFolder, () => null, null) expect(getFolder(newTree, '666').status).toEqual('checked') expect(getFile(newTree, '777').status).toEqual('checked') @@ -160,7 +160,7 @@ describe('afterClickOnFolder', () => { const clickedFolder = oldPartialTree.find((f) => f.id === '2') as PartialTreeFolderNode - const newTree = afterClickOnFolder(oldPartialTree, fakeCompanionFiles, clickedFolder, () => null, null) + const newTree = afterOpenFolder(oldPartialTree, fakeCompanionFiles, clickedFolder, () => null, null) expect(getFolder(newTree, '666').status).toEqual('unchecked') expect(getFile(newTree, '777').status).toEqual('unchecked') diff --git a/packages/@uppy/provider-views/src/utils/PartialTreeUtils/index.ts b/packages/@uppy/provider-views/src/utils/PartialTreeUtils/index.ts index 8749e5e88d..0dabc560a8 100644 --- a/packages/@uppy/provider-views/src/utils/PartialTreeUtils/index.ts +++ b/packages/@uppy/provider-views/src/utils/PartialTreeUtils/index.ts @@ -1,10 +1,10 @@ -import afterClickOnFolder from './afterClickOnFolder' +import afterOpenFolder from './afterOpenFolder' import afterScroll from './afterScroll' import afterToggleCheckbox from './afterToggleCheckbox' import fill from './fill' export default { - afterClickOnFolder, + afterOpenFolder, afterScroll, afterToggleCheckbox, fill From 39509330427940935e628f4c954f1e425d7410b0 Mon Sep 17 00:00:00 2001 From: Evgenia Karunus Date: Sat, 27 Apr 2024 02:15:19 +0400 Subject: [PATCH 094/170] tests - wrote tests for `afterScrollFolder.ts` --- .../src/ProviderView/ProviderView.tsx | 2 +- .../{afterScroll.ts => afterScrollFolder.ts} | 4 +- .../src/utils/PartialTreeUtils/index.test.ts | 41 +++++++++++++++++++ .../src/utils/PartialTreeUtils/index.ts | 4 +- 4 files changed, 46 insertions(+), 5 deletions(-) rename packages/@uppy/provider-views/src/utils/PartialTreeUtils/{afterScroll.ts => afterScrollFolder.ts} (96%) diff --git a/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx b/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx index 0f30a17a10..ff06da7c85 100644 --- a/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx +++ b/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx @@ -263,7 +263,7 @@ export default class ProviderView{ this.isHandlingScroll = true await this.#withAbort(async (signal) => { const { nextPagePath, items } = await this.provider.list(currentFolder.nextPagePath!, { signal }) - const newPartialTree = PartialTreeUtils.afterScroll(partialTree, currentFolderId, items, nextPagePath, validateRestrictions(this.plugin)) + const newPartialTree = PartialTreeUtils.afterScrollFolder(partialTree, currentFolderId, items, nextPagePath, validateRestrictions(this.plugin)) this.plugin.setPluginState({ partialTree: newPartialTree }) }).catch(handleError(this.plugin.uppy)) diff --git a/packages/@uppy/provider-views/src/utils/PartialTreeUtils/afterScroll.ts b/packages/@uppy/provider-views/src/utils/PartialTreeUtils/afterScrollFolder.ts similarity index 96% rename from packages/@uppy/provider-views/src/utils/PartialTreeUtils/afterScroll.ts rename to packages/@uppy/provider-views/src/utils/PartialTreeUtils/afterScrollFolder.ts index cfeda7200f..424ece7e63 100644 --- a/packages/@uppy/provider-views/src/utils/PartialTreeUtils/afterScroll.ts +++ b/packages/@uppy/provider-views/src/utils/PartialTreeUtils/afterScrollFolder.ts @@ -1,7 +1,7 @@ import type { PartialTree, PartialTreeFile, PartialTreeFolder, PartialTreeFolderNode } from "@uppy/core/lib/Uppy" import type { CompanionFile } from "@uppy/utils/lib/CompanionFile" -const afterScroll = ( +const afterScrollFolder = ( oldPartialTree: PartialTree, currentFolderId: string | null, items: CompanionFile[], @@ -47,4 +47,4 @@ const afterScroll = ( return newPartialTree } -export default afterScroll +export default afterScrollFolder diff --git a/packages/@uppy/provider-views/src/utils/PartialTreeUtils/index.test.ts b/packages/@uppy/provider-views/src/utils/PartialTreeUtils/index.test.ts index 98694a4936..fb34f7c25b 100644 --- a/packages/@uppy/provider-views/src/utils/PartialTreeUtils/index.test.ts +++ b/packages/@uppy/provider-views/src/utils/PartialTreeUtils/index.test.ts @@ -3,6 +3,7 @@ import afterToggleCheckbox from './afterToggleCheckbox.ts' import type { PartialTree, PartialTreeFile, PartialTreeFolderNode, PartialTreeFolderRoot } from '@uppy/core/lib/Uppy.ts' import type { CompanionFile } from '@uppy/utils/lib/CompanionFile' import afterOpenFolder from './afterOpenFolder.ts' +import afterScrollFolder from './afterScrollFolderFolder.ts' const _root = (id: string, options: any = {}) : PartialTreeFolderRoot => ({ type: 'root', @@ -167,3 +168,43 @@ describe('afterOpenFolder', () => { expect(getFile(newTree, '888').status).toEqual('unchecked') }) }) + +describe('afterScrollFolder', () => { + it('scroll "checked" folder - all discovered files are marked as "checked"', () => { + const oldPartialTree : PartialTree = [ + _root('ourRoot'), + _folder('1', { parentId: 'ourRoot' }), + _folder('2', { parentId: 'ourRoot', cached: true, status: 'checked' }), + _file('2_1', { parentId: '2' }), + _file('2_2', { parentId: '2' }), + _file('2_3', { parentId: '2' }), + ] + + const fakeCompanionFiles = [{ requestPath: '666', isFolder: true }, { requestPath: '777', isFolder: false }, { requestPath: '888', isFolder: false }] as CompanionFile[] + + const newTree = afterScrollFolder(oldPartialTree, '2', fakeCompanionFiles, null, () => null) + + expect(getFolder(newTree, '666').status).toEqual('checked') + expect(getFile(newTree, '777').status).toEqual('checked') + expect(getFile(newTree, '888').status).toEqual('checked') + }) + + it('scroll "checked" folder - all discovered files are marked as "unchecked"', () => { + const oldPartialTree : PartialTree = [ + _root('ourRoot'), + _folder('1', { parentId: 'ourRoot' }), + _folder('2', { parentId: 'ourRoot', cached: true, status: 'unchecked' }), + _file('2_1', { parentId: '2' }), + _file('2_2', { parentId: '2' }), + _file('2_3', { parentId: '2' }), + ] + + const fakeCompanionFiles = [{ requestPath: '666', isFolder: true }, { requestPath: '777', isFolder: false }, { requestPath: '888', isFolder: false }] as CompanionFile[] + + const newTree = afterScrollFolder(oldPartialTree, '2', fakeCompanionFiles, null, () => null) + + expect(getFolder(newTree, '666').status).toEqual('unchecked') + expect(getFile(newTree, '777').status).toEqual('unchecked') + expect(getFile(newTree, '888').status).toEqual('unchecked') + }) +}) diff --git a/packages/@uppy/provider-views/src/utils/PartialTreeUtils/index.ts b/packages/@uppy/provider-views/src/utils/PartialTreeUtils/index.ts index 0dabc560a8..7b50f5f27f 100644 --- a/packages/@uppy/provider-views/src/utils/PartialTreeUtils/index.ts +++ b/packages/@uppy/provider-views/src/utils/PartialTreeUtils/index.ts @@ -1,11 +1,11 @@ import afterOpenFolder from './afterOpenFolder' -import afterScroll from './afterScroll' +import afterScrollFolder from './afterScrollFolder' import afterToggleCheckbox from './afterToggleCheckbox' import fill from './fill' export default { afterOpenFolder, - afterScroll, + afterScrollFolder, afterToggleCheckbox, fill } From f6476e0e79b52027929d521943cdf11d56e0887b Mon Sep 17 00:00:00 2001 From: Evgenia Karunus Date: Mon, 29 Apr 2024 04:34:44 +0400 Subject: [PATCH 095/170] getPaths.ts - make `absDirPath`, `relDirPath` work like in docs & add tests for that --- .../src/utils/PartialTreeUtils/fill.ts | 29 ++-------- .../src/utils/PartialTreeUtils/getPaths.ts | 31 +++++++++++ .../src/utils/PartialTreeUtils/index.test.ts | 54 ++++++++++++++++--- .../provider-views/src/utils/getTagFile.ts | 15 ++---- 4 files changed, 86 insertions(+), 43 deletions(-) create mode 100644 packages/@uppy/provider-views/src/utils/PartialTreeUtils/getPaths.ts diff --git a/packages/@uppy/provider-views/src/utils/PartialTreeUtils/fill.ts b/packages/@uppy/provider-views/src/utils/PartialTreeUtils/fill.ts index 7ee3840d6d..f5b5284e2e 100644 --- a/packages/@uppy/provider-views/src/utils/PartialTreeUtils/fill.ts +++ b/packages/@uppy/provider-views/src/utils/PartialTreeUtils/fill.ts @@ -2,24 +2,7 @@ import type { PartialTree, PartialTreeFile, PartialTreeFolder, PartialTreeFolder import type { CompanionClientProvider, RequestOptions } from "@uppy/utils/lib/CompanionClientProvider" import type { CompanionFile } from "@uppy/utils/lib/CompanionFile" import PQueue from "p-queue" - -const getAbsPath = (partialTree: PartialTree, file: PartialTreeFile) : (PartialTreeFile | PartialTreeFolderNode)[] => { - const path : (PartialTreeFile | PartialTreeFolderNode)[] = [] - let parent: PartialTreeFile | PartialTreeFolder = file - while (true) { - if (parent.type === 'root') break - path.push(parent) - parent = partialTree.find((folder) => folder.id === (parent as PartialTreeFolderNode).parentId) as PartialTreeFolder - } - - return path.toReversed() -} - -const getRelPath = (absPath: (PartialTreeFile | PartialTreeFolderNode)[]) : (PartialTreeFile | PartialTreeFolderNode)[] => { - const firstCheckedFolderIndex = absPath.findIndex((i) => i.type === 'folder' && i.status === 'checked') - const relPath = absPath.slice(firstCheckedFolderIndex) - return relPath -} +import getPaths from "./getPaths" const recursivelyFetch = async (queue: PQueue, poorTree: PartialTree, poorFolder: PartialTreeFolderNode, provider: CompanionClientProvider, signal: AbortSignal): Promise => { let items : CompanionFile[] = [] @@ -91,14 +74,8 @@ const fill = async (partialTree: PartialTree, provider: CompanionClientProvider, const checkedFiles = poorTree.filter((item) => item.type === 'file' && item.status === 'checked') as PartialTreeFile[] const uppyFiles = checkedFiles.map((file) => { - const absPath = getAbsPath(poorTree, file) - const relPath = getRelPath(absPath) - - return { - ...file.data, - absDirPath: absPath.join('/'), - relDirPath: relPath.join('/') - } + const paths = getPaths(poorTree, file) + return { ...file.data, ...paths } }) return uppyFiles diff --git a/packages/@uppy/provider-views/src/utils/PartialTreeUtils/getPaths.ts b/packages/@uppy/provider-views/src/utils/PartialTreeUtils/getPaths.ts new file mode 100644 index 0000000000..6f3210c835 --- /dev/null +++ b/packages/@uppy/provider-views/src/utils/PartialTreeUtils/getPaths.ts @@ -0,0 +1,31 @@ +import type { PartialTree, PartialTreeFile, PartialTreeFolder, PartialTreeFolderNode } from "@uppy/core/lib/Uppy" + +// See "Uppy file properties" documentation for `.absolutePath` and `.relativePath` (https://uppy.io/docs/uppy/#working-with-uppy-files) +const getPaths = (partialTree: PartialTree, file: PartialTreeFile) : { absDirPath: string, relDirPath: string | undefined } => { + const path : (PartialTreeFile | PartialTreeFolderNode)[] = [] + let parent: PartialTreeFile | PartialTreeFolder = file + while (true) { + if (parent.type === 'root') break + path.push(parent) + parent = partialTree.find((folder) => folder.id === (parent as PartialTreeFolderNode).parentId) as PartialTreeFolder + } + + const absFolders = path.toReversed() + + const firstCheckedFolderIndex = absFolders.findIndex((i) => i.type === 'folder' && i.status === 'checked') + const relFolders = absFolders.slice(firstCheckedFolderIndex) + + const absDirPath = '/' + absFolders.map((i) => i.data.name).join('/') + const relDirPath = relFolders.length === 1 + // Must return null + // (https://github.com/transloadit/uppy/pull/4537#issuecomment-1629136652) + ? undefined + : relFolders.map((i) => i.data.name).join('/') + + return { + absDirPath, + relDirPath + } +} + +export default getPaths diff --git a/packages/@uppy/provider-views/src/utils/PartialTreeUtils/index.test.ts b/packages/@uppy/provider-views/src/utils/PartialTreeUtils/index.test.ts index fb34f7c25b..2fef8a4cb7 100644 --- a/packages/@uppy/provider-views/src/utils/PartialTreeUtils/index.test.ts +++ b/packages/@uppy/provider-views/src/utils/PartialTreeUtils/index.test.ts @@ -3,7 +3,9 @@ import afterToggleCheckbox from './afterToggleCheckbox.ts' import type { PartialTree, PartialTreeFile, PartialTreeFolderNode, PartialTreeFolderRoot } from '@uppy/core/lib/Uppy.ts' import type { CompanionFile } from '@uppy/utils/lib/CompanionFile' import afterOpenFolder from './afterOpenFolder.ts' -import afterScrollFolder from './afterScrollFolderFolder.ts' +import afterScrollFolder from './afterScrollFolder.ts' +import fill from './fill.ts' +import type { CompanionClientProvider } from '@uppy/utils/lib/CompanionClientProvider' const _root = (id: string, options: any = {}) : PartialTreeFolderRoot => ({ type: 'root', @@ -19,7 +21,7 @@ const _folder = (id: string, options: any) : PartialTreeFolderNode => ({ cached: true, nextPagePath: null, status: 'unchecked', - data: ({} as CompanionFile), + data: ({ id, name: `name_${id}` } as CompanionFile), ...options }) @@ -28,7 +30,7 @@ const _file = (id: string, options: any) : PartialTreeFile => ({ id, status: 'unchecked', parentId: options.parentId, - data: ({} as CompanionFile), + data: ({ id, name: `name_${id}.jpg` } as CompanionFile), ...options }) @@ -39,7 +41,7 @@ const getFolder = (tree: PartialTree, id: string) => const getFile = (tree: PartialTree, id: string) => tree.find((i) => i.id === id) as PartialTreeFile -describe('afterToggleCheckbox', () => { +describe('afterToggleCheckbox()', () => { const oldPartialTree : PartialTree = [ _root('ourRoot'), _folder('1', { parentId: 'ourRoot' }), @@ -131,7 +133,7 @@ describe('afterToggleCheckbox', () => { }) }) -describe('afterOpenFolder', () => { +describe('afterOpenFolder()', () => { it('open "checked" folder - all discovered files are marked as "checked"', () => { const oldPartialTree : PartialTree = [ _root('ourRoot'), @@ -169,7 +171,7 @@ describe('afterOpenFolder', () => { }) }) -describe('afterScrollFolder', () => { +describe('afterScrollFolder()', () => { it('scroll "checked" folder - all discovered files are marked as "checked"', () => { const oldPartialTree : PartialTree = [ _root('ourRoot'), @@ -208,3 +210,43 @@ describe('afterScrollFolder', () => { expect(getFile(newTree, '888').status).toEqual('unchecked') }) }) + +// Based on documentation for .absolutePath and .relativePath (https://uppy.io/docs/uppy/#filemeta) +describe('getPaths(): .absolutePath, .relativePath)', () => { + // Note that this is a tree that doesn't require any api calls, everything is cached already + const tree : PartialTree = [ + _root('ourRoot'), + _folder('1', { parentId: 'ourRoot' }), + _folder('2', { parentId: 'ourRoot' }), + _file('2_1', { parentId: '2' }), + _file('2_2', { parentId: '2', status: 'checked' }), + _file('2_3', { parentId: '2' }), + _folder('2_4', { parentId: '2', status: 'checked' }), + _file('2_4_1', { parentId: '2_4', status: 'checked' }), + _file('2_4_2', { parentId: '2_4', status: 'checked' }), + _file('2_4_3', { parentId: '2_4', status: 'checked' }), + _file('3', { parentId: 'ourRoot' }), + _file('4', { parentId: 'ourRoot' }), + ] + const fakeProvider = {} as CompanionClientProvider + const fakeSignal = {} as AbortSignal + + it('.absolutePath always begins with / + always ends with the file’s name.', async () => { + const result = await fill(tree, fakeProvider, fakeSignal) + + expect(result.find((f) => f.id === '2_2')!.absDirPath).toEqual('/name_2/name_2_2.jpg') + expect(result.find((f) => f.id === '2_4_3')!.absDirPath).toEqual('/name_2/name_2_4/name_2_4_3.jpg') + }) + + it('.relativePath is null when file is selected independently', async () => { + const result = await fill(tree, fakeProvider, fakeSignal) + + expect(result.find((f) => f.id === '2_2')!.relDirPath).toEqual(undefined) + }) + + it('.relativePath attends to highest checked folder', async () => { + const result = await fill(tree, fakeProvider, fakeSignal) + + expect(result.find((f) => f.id === '2_4_1')!.relDirPath).toEqual('name_2_4/name_2_4_1.jpg') + }) +}) diff --git a/packages/@uppy/provider-views/src/utils/getTagFile.ts b/packages/@uppy/provider-views/src/utils/getTagFile.ts index 8f5c670299..762ae8c20f 100644 --- a/packages/@uppy/provider-views/src/utils/getTagFile.ts +++ b/packages/@uppy/provider-views/src/utils/getTagFile.ts @@ -39,21 +39,14 @@ const getTagFile = (file: CompanionFile, pluginId: string, provi if (file.author) { if (file.author.name != null) - tagFile.meta!.authorName = String(file.author.name) - if (file.author.url) tagFile.meta!.authorUrl = file.author.url + tagFile.meta.authorName = String(file.author.name) + 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}` - + tagFile.meta.relativePath = file.relDirPath || null + tagFile.meta.absolutePath = file.absDirPath || `/${tagFile.name}` return tagFile } From 18ca4bb41fec47915f0167f352838c6cd15be4a2 Mon Sep 17 00:00:00 2001 From: Evgenia Karunus Date: Mon, 29 Apr 2024 06:24:17 +0400 Subject: [PATCH 096/170] injectPaths.ts - improve performance --- .../src/utils/PartialTreeUtils/fill.ts | 11 ++-- .../src/utils/PartialTreeUtils/getPaths.ts | 31 ----------- .../src/utils/PartialTreeUtils/index.test.ts | 19 +++---- .../src/utils/PartialTreeUtils/injectPaths.ts | 54 +++++++++++++++++++ 4 files changed, 67 insertions(+), 48 deletions(-) delete mode 100644 packages/@uppy/provider-views/src/utils/PartialTreeUtils/getPaths.ts create mode 100644 packages/@uppy/provider-views/src/utils/PartialTreeUtils/injectPaths.ts diff --git a/packages/@uppy/provider-views/src/utils/PartialTreeUtils/fill.ts b/packages/@uppy/provider-views/src/utils/PartialTreeUtils/fill.ts index f5b5284e2e..4e465dd8f9 100644 --- a/packages/@uppy/provider-views/src/utils/PartialTreeUtils/fill.ts +++ b/packages/@uppy/provider-views/src/utils/PartialTreeUtils/fill.ts @@ -2,7 +2,7 @@ import type { PartialTree, PartialTreeFile, PartialTreeFolder, PartialTreeFolder import type { CompanionClientProvider, RequestOptions } from "@uppy/utils/lib/CompanionClientProvider" import type { CompanionFile } from "@uppy/utils/lib/CompanionFile" import PQueue from "p-queue" -import getPaths from "./getPaths" +import injectPaths from "./injectPaths" const recursivelyFetch = async (queue: PQueue, poorTree: PartialTree, poorFolder: PartialTreeFolderNode, provider: CompanionClientProvider, signal: AbortSignal): Promise => { let items : CompanionFile[] = [] @@ -72,13 +72,8 @@ const fill = async (partialTree: PartialTree, provider: CompanionClientProvider, // Return all 'checked' files const checkedFiles = poorTree.filter((item) => item.type === 'file' && item.status === 'checked') as PartialTreeFile[] - - const uppyFiles = checkedFiles.map((file) => { - const paths = getPaths(poorTree, file) - return { ...file.data, ...paths } - }) - + const uppyFiles = injectPaths(poorTree, checkedFiles) return uppyFiles } -export default fill; +export default fill diff --git a/packages/@uppy/provider-views/src/utils/PartialTreeUtils/getPaths.ts b/packages/@uppy/provider-views/src/utils/PartialTreeUtils/getPaths.ts deleted file mode 100644 index 6f3210c835..0000000000 --- a/packages/@uppy/provider-views/src/utils/PartialTreeUtils/getPaths.ts +++ /dev/null @@ -1,31 +0,0 @@ -import type { PartialTree, PartialTreeFile, PartialTreeFolder, PartialTreeFolderNode } from "@uppy/core/lib/Uppy" - -// See "Uppy file properties" documentation for `.absolutePath` and `.relativePath` (https://uppy.io/docs/uppy/#working-with-uppy-files) -const getPaths = (partialTree: PartialTree, file: PartialTreeFile) : { absDirPath: string, relDirPath: string | undefined } => { - const path : (PartialTreeFile | PartialTreeFolderNode)[] = [] - let parent: PartialTreeFile | PartialTreeFolder = file - while (true) { - if (parent.type === 'root') break - path.push(parent) - parent = partialTree.find((folder) => folder.id === (parent as PartialTreeFolderNode).parentId) as PartialTreeFolder - } - - const absFolders = path.toReversed() - - const firstCheckedFolderIndex = absFolders.findIndex((i) => i.type === 'folder' && i.status === 'checked') - const relFolders = absFolders.slice(firstCheckedFolderIndex) - - const absDirPath = '/' + absFolders.map((i) => i.data.name).join('/') - const relDirPath = relFolders.length === 1 - // Must return null - // (https://github.com/transloadit/uppy/pull/4537#issuecomment-1629136652) - ? undefined - : relFolders.map((i) => i.data.name).join('/') - - return { - absDirPath, - relDirPath - } -} - -export default getPaths diff --git a/packages/@uppy/provider-views/src/utils/PartialTreeUtils/index.test.ts b/packages/@uppy/provider-views/src/utils/PartialTreeUtils/index.test.ts index 2fef8a4cb7..610effa1b8 100644 --- a/packages/@uppy/provider-views/src/utils/PartialTreeUtils/index.test.ts +++ b/packages/@uppy/provider-views/src/utils/PartialTreeUtils/index.test.ts @@ -6,6 +6,7 @@ import afterOpenFolder from './afterOpenFolder.ts' import afterScrollFolder from './afterScrollFolder.ts' import fill from './fill.ts' import type { CompanionClientProvider } from '@uppy/utils/lib/CompanionClientProvider' +import injectPaths from './injectPaths.ts' const _root = (id: string, options: any = {}) : PartialTreeFolderRoot => ({ type: 'root', @@ -212,7 +213,7 @@ describe('afterScrollFolder()', () => { }) // Based on documentation for .absolutePath and .relativePath (https://uppy.io/docs/uppy/#filemeta) -describe('getPaths(): .absolutePath, .relativePath)', () => { +describe('injectPaths()', () => { // Note that this is a tree that doesn't require any api calls, everything is cached already const tree : PartialTree = [ _root('ourRoot'), @@ -228,25 +229,25 @@ describe('getPaths(): .absolutePath, .relativePath)', () => { _file('3', { parentId: 'ourRoot' }), _file('4', { parentId: 'ourRoot' }), ] - const fakeProvider = {} as CompanionClientProvider - const fakeSignal = {} as AbortSignal + const checkedFiles = tree.filter((item) => item.type === 'file' && item.status === 'checked') as PartialTreeFile[] - it('.absolutePath always begins with / + always ends with the file’s name.', async () => { - const result = await fill(tree, fakeProvider, fakeSignal) + it('.absolutePath always begins with / + always ends with the file’s name.', () => { + const result = injectPaths(tree, checkedFiles) expect(result.find((f) => f.id === '2_2')!.absDirPath).toEqual('/name_2/name_2_2.jpg') expect(result.find((f) => f.id === '2_4_3')!.absDirPath).toEqual('/name_2/name_2_4/name_2_4_3.jpg') }) - it('.relativePath is null when file is selected independently', async () => { - const result = await fill(tree, fakeProvider, fakeSignal) + it('.relativePath is null when file is selected independently', () => { + const result = injectPaths(tree, checkedFiles) expect(result.find((f) => f.id === '2_2')!.relDirPath).toEqual(undefined) }) - it('.relativePath attends to highest checked folder', async () => { - const result = await fill(tree, fakeProvider, fakeSignal) + it('.relativePath attends to highest checked folder', () => { + const result = injectPaths(tree, checkedFiles) expect(result.find((f) => f.id === '2_4_1')!.relDirPath).toEqual('name_2_4/name_2_4_1.jpg') }) + }) diff --git a/packages/@uppy/provider-views/src/utils/PartialTreeUtils/injectPaths.ts b/packages/@uppy/provider-views/src/utils/PartialTreeUtils/injectPaths.ts new file mode 100644 index 0000000000..f9bb170d0f --- /dev/null +++ b/packages/@uppy/provider-views/src/utils/PartialTreeUtils/injectPaths.ts @@ -0,0 +1,54 @@ +import type { PartialTree, PartialTreeFile, PartialTreeFolderNode, PartialTreeId } from "@uppy/core/lib/Uppy" +import type { CompanionFile } from "@uppy/utils/lib/CompanionFile" + +export interface Cache { + [key: string]: (PartialTreeFile | PartialTreeFolderNode)[] +} + +const getPath = ( + partialTree: PartialTree, + id: PartialTreeId, + cache: Cache +) : (PartialTreeFile | PartialTreeFolderNode)[] => { + const sId = id === null ? 'null' : id + if (cache[sId]) return cache[sId] + + const file = partialTree.find((f) => f.id === id)! + + if (file.type === 'root') return [] + + const meAndParentPath = [file, ...getPath(partialTree, file.parentId, cache)] + cache[sId] = meAndParentPath + return meAndParentPath +} + +// See "Uppy file properties" documentation for `.absolutePath` and `.relativePath` (https://uppy.io/docs/uppy/#working-with-uppy-files) +const injectPaths = (partialTree: PartialTree, files: PartialTreeFile[]) : CompanionFile[] => { + const cache : Cache = {} + + const injectedFiles = files.map((file) => { + const path : (PartialTreeFile | PartialTreeFolderNode)[] = getPath(partialTree, file.id, cache) + + const absFolders = path.toReversed() + + const firstCheckedFolderIndex = absFolders.findIndex((i) => i.type === 'folder' && i.status === 'checked') + const relFolders = absFolders.slice(firstCheckedFolderIndex) + + const absDirPath = '/' + absFolders.map((i) => i.data.name).join('/') + const relDirPath = relFolders.length === 1 + // Must return null + // (https://github.com/transloadit/uppy/pull/4537#issuecomment-1629136652) + ? undefined + : relFolders.map((i) => i.data.name).join('/') + + return { + ...file.data, + absDirPath, + relDirPath + } + }) + + return injectedFiles +} + +export default injectPaths From 8b5f0335490443f8a982c21ce53e0ff91f40ef6c Mon Sep 17 00:00:00 2001 From: Evgenia Karunus Date: Tue, 30 Apr 2024 03:14:47 +0400 Subject: [PATCH 097/170] getTagFile.ts - handle path injection all in one place --- .../src/SearchProviderView/SearchProviderView.tsx | 8 +++++--- packages/@uppy/provider-views/src/utils/getTagFile.ts | 7 ++++--- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/packages/@uppy/provider-views/src/SearchProviderView/SearchProviderView.tsx b/packages/@uppy/provider-views/src/SearchProviderView/SearchProviderView.tsx index 87591b1be7..06c2cbe4cd 100644 --- a/packages/@uppy/provider-views/src/SearchProviderView/SearchProviderView.tsx +++ b/packages/@uppy/provider-views/src/SearchProviderView/SearchProviderView.tsx @@ -16,6 +16,7 @@ import shouldHandleScroll from '../utils/shouldHandleScroll.ts' import handleError from '../utils/handleError.ts' import validateRestrictions from '../utils/validateRestrictions.ts' import getClickedRange from '../utils/getClickedRange.ts' +import injectPaths from '../utils/PartialTreeUtils/injectPaths.ts' const defaultState : UnknownSearchProviderPluginState = { loading: false, @@ -188,9 +189,10 @@ export default class SearchProviderView { donePicking(): void { const { partialTree } = this.plugin.getPluginState() this.plugin.uppy.log('Adding remote search provider files') - const files = partialTree.filter((i) => i.type !== 'root' && i.status === 'checked') as PartialTreeFile[] - const tagFiles = files.map((file) => - getTagFile(file.data, this.plugin.id, this.provider, this.plugin.opts.companionUrl) + const checkedFiles = partialTree.filter((i) => i.type !== 'root' && i.status === 'checked') as PartialTreeFile[] + const withPaths = injectPaths(partialTree, checkedFiles) + const tagFiles = withPaths.map((file) => + getTagFile(file, this.plugin.id, this.provider, this.plugin.opts.companionUrl) ) this.plugin.uppy.addFiles(tagFiles) diff --git a/packages/@uppy/provider-views/src/utils/getTagFile.ts b/packages/@uppy/provider-views/src/utils/getTagFile.ts index 762ae8c20f..089a8527ce 100644 --- a/packages/@uppy/provider-views/src/utils/getTagFile.ts +++ b/packages/@uppy/provider-views/src/utils/getTagFile.ts @@ -43,10 +43,11 @@ const getTagFile = (file: CompanionFile, pluginId: string, provi 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 - // and absolutePath (with leading slash) https://github.com/transloadit/uppy/pull/4537#issuecomment-1614236655 + // We need to do this `|| null` + // because .relDirPath is `undefined` and .relativePath is `null`. + // I do think we should just use `null` everywhere. tagFile.meta.relativePath = file.relDirPath || null - tagFile.meta.absolutePath = file.absDirPath || `/${tagFile.name}` + tagFile.meta.absolutePath = file.absDirPath return tagFile } From 36636382ad383a279dc6bc5ba978d1ecda8dd71b Mon Sep 17 00:00:00 2001 From: Evgenia Karunus Date: Tue, 30 Apr 2024 03:42:00 +0400 Subject: [PATCH 098/170] getTagFile.ts - refactor Just makes it easier to read the structure of TagFile --- .../provider-views/src/utils/getTagFile.ts | 32 ++++++++----------- 1 file changed, 13 insertions(+), 19 deletions(-) diff --git a/packages/@uppy/provider-views/src/utils/getTagFile.ts b/packages/@uppy/provider-views/src/utils/getTagFile.ts index 089a8527ce..af37275fa0 100644 --- a/packages/@uppy/provider-views/src/utils/getTagFile.ts +++ b/packages/@uppy/provider-views/src/utils/getTagFile.ts @@ -6,6 +6,8 @@ import isPreviewSupported from "@uppy/utils/lib/isPreviewSupported" // TODO: document what is a "tagFile" or get rid of this concept const getTagFile = (file: CompanionFile, pluginId: string, provider: CompanionClientProvider | CompanionClientSearchProvider, companionUrl: string) : TagFile => { + const fileType = getFileType({ type: file.mimeType, name: file.name }) + const tagFile: TagFile = { id: file.id, source: pluginId, @@ -13,7 +15,17 @@ const getTagFile = (file: CompanionFile, pluginId: string, provi type: file.mimeType, isRemote: true, data: file, - meta: {}, + // TODO Should we just always use the thumbnail URL if it exists? + preview: isPreviewSupported(fileType) ? file.thumbnail : undefined, + meta: { + authorName: file.author?.name, + authorUrl: file.author?.url, + // We need to do this `|| null` check, because null value + // for .relDirPath is `undefined` and for .relativePath is `null`. + // I do think we should just use `null` everywhere. + relativePath: file.relDirPath || null, + absolutePath: file.absDirPath + }, body: { fileId: file.id, }, @@ -30,24 +42,6 @@ const getTagFile = (file: CompanionFile, pluginId: string, provi }, } - const fileType = getFileType(tagFile) - - // TODO Should we just always use the thumbnail URL if it exists? - if (fileType && isPreviewSupported(fileType)) { - tagFile.preview = file.thumbnail - } - - if (file.author) { - if (file.author.name != null) - tagFile.meta.authorName = String(file.author.name) - if (file.author.url) tagFile.meta.authorUrl = file.author.url - } - - // We need to do this `|| null` - // because .relDirPath is `undefined` and .relativePath is `null`. - // I do think we should just use `null` everywhere. - tagFile.meta.relativePath = file.relDirPath || null - tagFile.meta.absolutePath = file.absDirPath return tagFile } From c9ca3b029b3b163c866e8a368809b13dfbe52473 Mon Sep 17 00:00:00 2001 From: Evgenia Karunus Date: Tue, 30 Apr 2024 04:01:48 +0400 Subject: [PATCH 099/170] fill.ts - `provider.list(currentPath, { signal })` => `apiList` (remove the dependency on provider, just pass a callback) --- .../src/ProviderView/ProviderView.tsx | 6 +++++- .../src/utils/PartialTreeUtils/fill.ts | 20 ++++++++++++------- .../src/utils/PartialTreeUtils/index.test.ts | 6 ++---- 3 files changed, 20 insertions(+), 12 deletions(-) diff --git a/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx b/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx index ff06da7c85..3850d47200 100644 --- a/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx +++ b/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx @@ -8,6 +8,7 @@ import type { PartialTreeFolderNode, PartialTreeFile, UnknownProviderPluginState, + PartialTreeId, } from '@uppy/core/lib/Uppy.ts' import type { Body, Meta, TagFile } from '@uppy/utils/lib/UppyFile' import type { CompanionFile } from '@uppy/utils/lib/CompanionFile.ts' @@ -276,7 +277,10 @@ export default class ProviderView{ this.setLoading(true) await this.#withAbort(async (signal) => { - const uppyFiles: CompanionFile[] = await PartialTreeUtils.fill(partialTree, this.provider, signal) + const uppyFiles: CompanionFile[] = await PartialTreeUtils.fill( + partialTree, + (path: PartialTreeId) => this.provider.list(path, { signal }) + ) const filesToAdd : TagFile[] = [] const filesAlreadyAdded : TagFile[] = [] diff --git a/packages/@uppy/provider-views/src/utils/PartialTreeUtils/fill.ts b/packages/@uppy/provider-views/src/utils/PartialTreeUtils/fill.ts index 4e465dd8f9..516ff60586 100644 --- a/packages/@uppy/provider-views/src/utils/PartialTreeUtils/fill.ts +++ b/packages/@uppy/provider-views/src/utils/PartialTreeUtils/fill.ts @@ -1,14 +1,20 @@ -import type { PartialTree, PartialTreeFile, PartialTreeFolder, PartialTreeFolderNode, PartialTreeId } from "@uppy/core/lib/Uppy" -import type { CompanionClientProvider, RequestOptions } from "@uppy/utils/lib/CompanionClientProvider" +import type { PartialTree, PartialTreeFile, PartialTreeFolderNode, PartialTreeId } from "@uppy/core/lib/Uppy" import type { CompanionFile } from "@uppy/utils/lib/CompanionFile" import PQueue from "p-queue" import injectPaths from "./injectPaths" -const recursivelyFetch = async (queue: PQueue, poorTree: PartialTree, poorFolder: PartialTreeFolderNode, provider: CompanionClientProvider, signal: AbortSignal): Promise => { +interface ApiList { + (directory: PartialTreeId): Promise<{ + nextPagePath: PartialTreeId + items: CompanionFile[] + }> +} + +const recursivelyFetch = async (queue: PQueue, poorTree: PartialTree, poorFolder: PartialTreeFolderNode, apiList: ApiList): Promise => { let items : CompanionFile[] = [] let currentPath : PartialTreeId = poorFolder.cached ? poorFolder.nextPagePath : poorFolder.id while (currentPath) { - const response = await provider.list(currentPath, { signal }) + const response = await apiList(currentPath) console.log({ currentPath, response }); items = items.concat(response.items) currentPath = response.nextPagePath @@ -43,14 +49,14 @@ const recursivelyFetch = async (queue: PQueue, poorTree: PartialTree, poorFolder folders.forEach(async (folder) => { queue.add(async () => - await recursivelyFetch(queue, poorTree, folder, provider, signal) + await recursivelyFetch(queue, poorTree, folder, apiList) ) }) return [] } -const fill = async (partialTree: PartialTree, provider: CompanionClientProvider, signal: AbortSignal) : Promise => { +const fill = async (partialTree: PartialTree, apiList: ApiList) : Promise => { const queue = new PQueue({ concurrency: 6 }) // fill up the missing parts of a partialTree! @@ -64,7 +70,7 @@ const fill = async (partialTree: PartialTree, provider: CompanionClientProvider, // per each poor folder, recursively fetch all files and make them .checked!!! poorFolders.forEach((poorFolder) => { queue.add(async () => - await recursivelyFetch(queue, poorTree, poorFolder, provider, signal) + await recursivelyFetch(queue, poorTree, poorFolder, apiList) ) }) diff --git a/packages/@uppy/provider-views/src/utils/PartialTreeUtils/index.test.ts b/packages/@uppy/provider-views/src/utils/PartialTreeUtils/index.test.ts index 610effa1b8..39cbe0d27c 100644 --- a/packages/@uppy/provider-views/src/utils/PartialTreeUtils/index.test.ts +++ b/packages/@uppy/provider-views/src/utils/PartialTreeUtils/index.test.ts @@ -35,8 +35,6 @@ const _file = (id: string, options: any) : PartialTreeFile => ({ ...options }) - - const getFolder = (tree: PartialTree, id: string) => tree.find((i) => i.id === id) as PartialTreeFolderNode const getFile = (tree: PartialTree, id: string) => @@ -212,7 +210,6 @@ describe('afterScrollFolder()', () => { }) }) -// Based on documentation for .absolutePath and .relativePath (https://uppy.io/docs/uppy/#filemeta) describe('injectPaths()', () => { // Note that this is a tree that doesn't require any api calls, everything is cached already const tree : PartialTree = [ @@ -231,6 +228,7 @@ describe('injectPaths()', () => { ] const checkedFiles = tree.filter((item) => item.type === 'file' && item.status === 'checked') as PartialTreeFile[] + // These test cases are based on documentation for .absolutePath and .relativePath (https://uppy.io/docs/uppy/#filemeta) it('.absolutePath always begins with / + always ends with the file’s name.', () => { const result = injectPaths(tree, checkedFiles) @@ -241,6 +239,7 @@ describe('injectPaths()', () => { it('.relativePath is null when file is selected independently', () => { const result = injectPaths(tree, checkedFiles) + // .relDirPath should be `undefined`, which will make .relativePath `null` eventually expect(result.find((f) => f.id === '2_2')!.relDirPath).toEqual(undefined) }) @@ -249,5 +248,4 @@ describe('injectPaths()', () => { expect(result.find((f) => f.id === '2_4_1')!.relDirPath).toEqual('name_2_4/name_2_4_1.jpg') }) - }) From 6b74df007ab6f6cb660939a9f019adf38c484e3d Mon Sep 17 00:00:00 2001 From: Evgenia Karunus Date: Wed, 1 May 2024 03:19:42 +0400 Subject: [PATCH 100/170] tests - wrote tests for `fill.ts` --- .../src/utils/PartialTreeUtils/fill.ts | 13 +- .../src/utils/PartialTreeUtils/index.test.ts | 154 +++++++++++++++++- 2 files changed, 152 insertions(+), 15 deletions(-) diff --git a/packages/@uppy/provider-views/src/utils/PartialTreeUtils/fill.ts b/packages/@uppy/provider-views/src/utils/PartialTreeUtils/fill.ts index 516ff60586..fbf21c181e 100644 --- a/packages/@uppy/provider-views/src/utils/PartialTreeUtils/fill.ts +++ b/packages/@uppy/provider-views/src/utils/PartialTreeUtils/fill.ts @@ -10,12 +10,11 @@ interface ApiList { }> } -const recursivelyFetch = async (queue: PQueue, poorTree: PartialTree, poorFolder: PartialTreeFolderNode, apiList: ApiList): Promise => { +const recursivelyFetch = async (queue: PQueue, poorTree: PartialTree, poorFolder: PartialTreeFolderNode, apiList: ApiList) => { let items : CompanionFile[] = [] let currentPath : PartialTreeId = poorFolder.cached ? poorFolder.nextPagePath : poorFolder.id while (currentPath) { const response = await apiList(currentPath) - console.log({ currentPath, response }); items = items.concat(response.items) currentPath = response.nextPagePath } @@ -48,12 +47,8 @@ const recursivelyFetch = async (queue: PQueue, poorTree: PartialTree, poorFolder poorTree.push(...files, ...folders) folders.forEach(async (folder) => { - queue.add(async () => - await recursivelyFetch(queue, poorTree, folder, apiList) - ) + queue.add(() => recursivelyFetch(queue, poorTree, folder, apiList)) }) - - return [] } const fill = async (partialTree: PartialTree, apiList: ApiList) : Promise => { @@ -69,9 +64,7 @@ const fill = async (partialTree: PartialTree, apiList: ApiList) : Promise { - queue.add(async () => - await recursivelyFetch(queue, poorTree, poorFolder, apiList) - ) + queue.add(() => recursivelyFetch(queue, poorTree, poorFolder, apiList)) }) await queue.onIdle() diff --git a/packages/@uppy/provider-views/src/utils/PartialTreeUtils/index.test.ts b/packages/@uppy/provider-views/src/utils/PartialTreeUtils/index.test.ts index 39cbe0d27c..db75017148 100644 --- a/packages/@uppy/provider-views/src/utils/PartialTreeUtils/index.test.ts +++ b/packages/@uppy/provider-views/src/utils/PartialTreeUtils/index.test.ts @@ -1,11 +1,10 @@ -import { describe, expect, it } from 'vitest' +import { describe, expect, it, vi } from 'vitest' import afterToggleCheckbox from './afterToggleCheckbox.ts' -import type { PartialTree, PartialTreeFile, PartialTreeFolderNode, PartialTreeFolderRoot } from '@uppy/core/lib/Uppy.ts' +import type { PartialTree, PartialTreeFile, PartialTreeFolderNode, PartialTreeFolderRoot, PartialTreeId } from '@uppy/core/lib/Uppy.ts' import type { CompanionFile } from '@uppy/utils/lib/CompanionFile' import afterOpenFolder from './afterOpenFolder.ts' import afterScrollFolder from './afterScrollFolder.ts' import fill from './fill.ts' -import type { CompanionClientProvider } from '@uppy/utils/lib/CompanionClientProvider' import injectPaths from './injectPaths.ts' const _root = (id: string, options: any = {}) : PartialTreeFolderRoot => ({ @@ -16,13 +15,27 @@ const _root = (id: string, options: any = {}) : PartialTreeFolderRoot => ({ ...options }) +const _cFile = (id: string) => ({ + id, + requestPath: id, + name: `name_${id}.jpg`, + isFolder: false +} as CompanionFile) + +const _cFolder = (id: string) => ({ + id, + requestPath: id, + name: `name_${id}`, + isFolder: true +} as CompanionFile) + const _folder = (id: string, options: any) : PartialTreeFolderNode => ({ type: 'folder', id, cached: true, nextPagePath: null, status: 'unchecked', - data: ({ id, name: `name_${id}` } as CompanionFile), + data: _cFolder(id), ...options }) @@ -31,7 +44,7 @@ const _file = (id: string, options: any) : PartialTreeFile => ({ id, status: 'unchecked', parentId: options.parentId, - data: ({ id, name: `name_${id}.jpg` } as CompanionFile), + data: _cFile(id), ...options }) @@ -249,3 +262,134 @@ describe('injectPaths()', () => { expect(result.find((f) => f.id === '2_4_1')!.relDirPath).toEqual('name_2_4/name_2_4_1.jpg') }) }) + +describe('fill()', () => { + it('fetches an already loaded file', async () => { + const tree : PartialTree = [ + _root('ourRoot'), + _folder('1', { parentId: 'ourRoot' }), + _folder('2', { parentId: 'ourRoot' }), + _file('2_1', { parentId: '2' }), + _file('2_2', { parentId: '2', status: 'checked' }), + _file('2_3', { parentId: '2' }), + _folder('2_4', { parentId: '2' }), + _file('3', { parentId: 'ourRoot' }), + _file('4', { parentId: 'ourRoot' }), + ] + const mock = vi.fn() + const result = await fill(tree, mock) + + // While we're at it - make sure we're not doing excessive api calls! + expect(mock.mock.calls.length).toEqual(0) + + expect(result.length).toEqual(1) + expect(result[0].id).toEqual('2_2') + }) + + it('fetches a .checked folder', async () => { + const tree : PartialTree = [ + _root('ourRoot'), + _folder('1', { parentId: 'ourRoot' }), + _folder('2', { parentId: 'ourRoot', cached: false, status: 'checked' }), + ] + const mock = (path: PartialTreeId) => { + if (path === '2') { + const items = [_cFile('2_1'), _cFile('2_2')] + return Promise.resolve({ nextPagePath: '666', items }) + } else if (path === '666') { + const items = [_cFile('2_3'), _cFile('2_4')] + return Promise.resolve({ nextPagePath: null, items }) + } + return Promise.reject() + } + const result = await fill(tree, mock) + + expect(result.length).toEqual(4) + expect(result.map((f) => f.id)).toEqual(['2_1', '2_2', '2_3', '2_4']) + }) + + it('fetches remaining pages in a folder', async () => { + const tree : PartialTree = [ + _root('ourRoot'), + _folder('1', { parentId: 'ourRoot' }), + _folder('2', { parentId: 'ourRoot', cached: true, nextPagePath: '666', status: 'checked' }), + ] + const mock = (path: PartialTreeId) => { + if (path === '666') { + const items = [_cFile('111'), _cFile('222')] + return Promise.resolve({ nextPagePath: null, items }) + } + return Promise.reject() + } + const result = await fill(tree, mock) + + expect(result.length).toEqual(2) + expect(result.map((f) => f.id)).toEqual(['111', '222']) + }) + + it('fetches a folder two levels deep', async () => { + const tree : PartialTree = [ + _root('ourRoot'), + _folder('1', { parentId: 'ourRoot' }), + _folder('2', { parentId: 'ourRoot', cached: true, nextPagePath: '2_next', status: 'checked' }), + _file('2_1', { parentId: '2', status: 'checked' }), + _file('2_2', { parentId: '2', status: 'checked' }) + ] + const mock = (path: PartialTreeId) => { + if (path === '2_next') { + const items = [_cFile('2_3'), _cFolder('666')] + return Promise.resolve({ nextPagePath: null, items }) + } else if (path === '666') { + const items = [_cFile('666_1'), _cFile('666_2')] + return Promise.resolve({ nextPagePath: null, items }) + } + return Promise.reject() + } + const result = await fill(tree, mock) + + expect(result.length).toEqual(5) + expect(result.map((f) => f.id)).toEqual(['2_1', '2_2', '2_3', '666_1', '666_2']) + }) + + it('complex situation', async () => { + const tree : PartialTree = [ + _root('ourRoot'), + _folder('1', { parentId: 'ourRoot' }), + // folder we'll be recursively fetching really deeply + _folder('2', { parentId: 'ourRoot', cached: true, nextPagePath: '2_next', status: 'checked' }), + _file('2_1', { parentId: '2', status: 'checked' }), + _file('2_2', { parentId: '2', status: 'checked' }), + // folder with only some files checked + _folder('3', { parentId: 'ourRoot', cached: true, status: 'partial' }), + // empty folder + _folder('0', { parentId: '3', cached: false, status: 'checked' }), + _file('3_1', { parentId: '3', status: 'checked' }), + _file('3_2', { parentId: '3', status: 'unchecked' }), + ] + const mock = (path: PartialTreeId) => { + if (path === '2_next') { + const items = [_cFile('2_3'), _cFolder('666')] + return Promise.resolve({ nextPagePath: null, items }) + } else if (path === '666') { + const items = [_cFile('666_1'), _cFolder('777')] + return Promise.resolve({ nextPagePath: null, items }) + } else if (path === '777') { + const items = [_cFile('777_1'), _cFolder('777_2')] + return Promise.resolve({ nextPagePath: null, items }) + } else if (path === '777_2') { + const items = [_cFile('777_2_1')] + return Promise.resolve({ nextPagePath: '777_2_next', items }) + } else if (path === '777_2_next') { + const items = [_cFile('777_2_1_1')] + return Promise.resolve({ nextPagePath: null, items }) + } else if (path === '0') { + return Promise.resolve({ nextPagePath: null, items: [] }) + } + return Promise.reject() + } + const result = await fill(tree, mock) + + expect(result.length).toEqual(8) + expect(result.map((f) => f.id)).toEqual(['2_1', '2_2', '3_1', '2_3', '666_1', '777_1', '777_2_1', '777_2_1_1']) + }) +}) From 834199e3cef881d0550f288cac5fa45448c6e54a Mon Sep 17 00:00:00 2001 From: Evgenia Karunus Date: Wed, 1 May 2024 03:57:36 +0400 Subject: [PATCH 101/170] tests - wrote tests for `getNOfSelectedFiles.ts` --- .../src/ProviderView/ProviderView.tsx | 2 +- .../SearchProviderView/SearchProviderView.tsx | 2 +- .../getNOfSelectedFiles.ts | 0 .../src/utils/PartialTreeUtils/index.test.ts | 31 +++++++++++++++++++ 4 files changed, 33 insertions(+), 2 deletions(-) rename packages/@uppy/provider-views/src/utils/{ => PartialTreeUtils}/getNOfSelectedFiles.ts (100%) diff --git a/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx b/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx index 3850d47200..c7d1b1a0d4 100644 --- a/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx +++ b/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx @@ -22,7 +22,7 @@ import Browser from '../Browser.tsx' import packageJson from '../../package.json' import PartialTreeUtils from '../utils/PartialTreeUtils' import getTagFile from '../utils/getTagFile.ts' -import getNOfSelectedFiles from '../utils/getNOfSelectedFiles.ts' +import getNOfSelectedFiles from '../utils/PartialTreeUtils/getNOfSelectedFiles.ts' import shouldHandleScroll from '../utils/shouldHandleScroll.ts' import handleError from '../utils/handleError.ts' import validateRestrictions from '../utils/validateRestrictions.ts' diff --git a/packages/@uppy/provider-views/src/SearchProviderView/SearchProviderView.tsx b/packages/@uppy/provider-views/src/SearchProviderView/SearchProviderView.tsx index 06c2cbe4cd..19bfad7e13 100644 --- a/packages/@uppy/provider-views/src/SearchProviderView/SearchProviderView.tsx +++ b/packages/@uppy/provider-views/src/SearchProviderView/SearchProviderView.tsx @@ -10,7 +10,7 @@ import Browser from '../Browser.tsx' // @ts-ignore We don't want TS to generate types for the package.json import packageJson from '../../package.json' import getTagFile from '../utils/getTagFile.ts' -import getNOfSelectedFiles from '../utils/getNOfSelectedFiles.ts' +import getNOfSelectedFiles from '../utils/PartialTreeUtils/getNOfSelectedFiles.ts' import PartialTreeUtils from '../utils/PartialTreeUtils' import shouldHandleScroll from '../utils/shouldHandleScroll.ts' import handleError from '../utils/handleError.ts' diff --git a/packages/@uppy/provider-views/src/utils/getNOfSelectedFiles.ts b/packages/@uppy/provider-views/src/utils/PartialTreeUtils/getNOfSelectedFiles.ts similarity index 100% rename from packages/@uppy/provider-views/src/utils/getNOfSelectedFiles.ts rename to packages/@uppy/provider-views/src/utils/PartialTreeUtils/getNOfSelectedFiles.ts diff --git a/packages/@uppy/provider-views/src/utils/PartialTreeUtils/index.test.ts b/packages/@uppy/provider-views/src/utils/PartialTreeUtils/index.test.ts index db75017148..d2968c5373 100644 --- a/packages/@uppy/provider-views/src/utils/PartialTreeUtils/index.test.ts +++ b/packages/@uppy/provider-views/src/utils/PartialTreeUtils/index.test.ts @@ -6,6 +6,7 @@ import afterOpenFolder from './afterOpenFolder.ts' import afterScrollFolder from './afterScrollFolder.ts' import fill from './fill.ts' import injectPaths from './injectPaths.ts' +import getNOfSelectedFiles from './getNOfSelectedFiles.ts' const _root = (id: string, options: any = {}) : PartialTreeFolderRoot => ({ type: 'root', @@ -393,3 +394,33 @@ describe('fill()', () => { expect(result.map((f) => f.id)).toEqual(['2_1', '2_2', '3_1', '2_3', '666_1', '777_1', '777_2_1', '777_2_1_1']) }) }) + +describe('getNOfSelectedFiles()', () => { + it('gets all leaf items', () => { + const tree : PartialTree = [ + _root('ourRoot'), + // leaf .checked folder + _folder('1', { parentId: 'ourRoot', cached: false, status: 'checked' }), + // NON-left .checked folder + _folder('2', { parentId: 'ourRoot', status: 'checked' }), + // leaf .checked file + _file('2_1', { parentId: '2', status: 'checked' }), + // leaf .checked file + _file('2_2', { parentId: '2', status: 'checked' }) + ] + const result = getNOfSelectedFiles(tree) + + expect(result).toEqual(3) + }) + + it('empty folder, even after being opened, counts as leaf node', () => { + const tree : PartialTree = [ + _root('ourRoot'), + // empty .checked .cached folder + _folder('1', { parentId: 'ourRoot', cached: true, status: 'checked' }), + ] + const result = getNOfSelectedFiles(tree) + // This should be "1" for more pleasant UI - if the user unchecks this folder, they should immediately see "Selected (1)" turning into "Selected (0)". + expect(result).toEqual(1) + }) +}) From 6dfbcd2b63a5a1e42b21049c8a2e577b55f3006c Mon Sep 17 00:00:00 2001 From: Evgenia Karunus Date: Wed, 1 May 2024 04:59:53 +0400 Subject: [PATCH 102/170] everywhere - change `JSON.stringify()` => `clone()` --- .../src/utils/PartialTreeUtils/afterToggleCheckbox.ts | 3 ++- .../provider-views/src/utils/PartialTreeUtils/clone.ts | 8 ++++++++ .../provider-views/src/utils/PartialTreeUtils/fill.ts | 3 ++- 3 files changed, 12 insertions(+), 2 deletions(-) create mode 100644 packages/@uppy/provider-views/src/utils/PartialTreeUtils/clone.ts diff --git a/packages/@uppy/provider-views/src/utils/PartialTreeUtils/afterToggleCheckbox.ts b/packages/@uppy/provider-views/src/utils/PartialTreeUtils/afterToggleCheckbox.ts index b54230730f..24e93250bf 100644 --- a/packages/@uppy/provider-views/src/utils/PartialTreeUtils/afterToggleCheckbox.ts +++ b/packages/@uppy/provider-views/src/utils/PartialTreeUtils/afterToggleCheckbox.ts @@ -1,12 +1,13 @@ import type { PartialTree, PartialTreeFile, PartialTreeFolder, PartialTreeFolderNode } from "@uppy/core/lib/Uppy" import type { CompanionFile } from "@uppy/utils/lib/CompanionFile" +import clone from "./clone" const afterToggleCheckbox = ( oldPartialTree: PartialTree, clickedRange: string[], validateRestrictions: (file: CompanionFile) => object | null, ) : PartialTree => { - const newPartialTree : PartialTree = JSON.parse(JSON.stringify(oldPartialTree)) + const newPartialTree : PartialTree = clone(oldPartialTree) const ourItem = newPartialTree.find((item) => item.id === clickedRange[0]) as PartialTreeFile | PartialTreeFolderNode // if newStatus is "checked" - percolate down "checked" diff --git a/packages/@uppy/provider-views/src/utils/PartialTreeUtils/clone.ts b/packages/@uppy/provider-views/src/utils/PartialTreeUtils/clone.ts new file mode 100644 index 0000000000..9d6836d4c1 --- /dev/null +++ b/packages/@uppy/provider-views/src/utils/PartialTreeUtils/clone.ts @@ -0,0 +1,8 @@ +import type { PartialTree } from "@uppy/core/lib/Uppy"; + +// One-level copying is enough, because we're never mutating `.data = { THIS }` within our `partialTree` - we're only ever mutating stuff like `.status`, `.cached`, `.nextPagePath`. +const clone = (partialTree: PartialTree) => { + return partialTree.map((item) => ({ ...item })) +} + +export default clone diff --git a/packages/@uppy/provider-views/src/utils/PartialTreeUtils/fill.ts b/packages/@uppy/provider-views/src/utils/PartialTreeUtils/fill.ts index fbf21c181e..b4b67a96a1 100644 --- a/packages/@uppy/provider-views/src/utils/PartialTreeUtils/fill.ts +++ b/packages/@uppy/provider-views/src/utils/PartialTreeUtils/fill.ts @@ -2,6 +2,7 @@ import type { PartialTree, PartialTreeFile, PartialTreeFolderNode, PartialTreeId import type { CompanionFile } from "@uppy/utils/lib/CompanionFile" import PQueue from "p-queue" import injectPaths from "./injectPaths" +import clone from "./clone" interface ApiList { (directory: PartialTreeId): Promise<{ @@ -55,7 +56,7 @@ const fill = async (partialTree: PartialTree, apiList: ApiList) : Promise item.type === 'folder' && item.status === 'checked' && From 8e328fcc9650d2f65c9e79678e031707c5246100 Mon Sep 17 00:00:00 2001 From: Evgenia Karunus Date: Wed, 1 May 2024 05:03:42 +0400 Subject: [PATCH 103/170] `PartialTreeUtils.ts` - more consistent function naming + alphabetical order in tests --- .../src/ProviderView/ProviderView.tsx | 2 +- .../{fill.ts => afterFill.ts} | 4 +- .../src/utils/PartialTreeUtils/index.test.ts | 434 +++++++++--------- .../src/utils/PartialTreeUtils/index.ts | 4 +- 4 files changed, 222 insertions(+), 222 deletions(-) rename packages/@uppy/provider-views/src/utils/PartialTreeUtils/{fill.ts => afterFill.ts} (95%) diff --git a/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx b/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx index c7d1b1a0d4..b3b255eadc 100644 --- a/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx +++ b/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx @@ -277,7 +277,7 @@ export default class ProviderView{ this.setLoading(true) await this.#withAbort(async (signal) => { - const uppyFiles: CompanionFile[] = await PartialTreeUtils.fill( + const uppyFiles: CompanionFile[] = await PartialTreeUtils.afterFill( partialTree, (path: PartialTreeId) => this.provider.list(path, { signal }) ) diff --git a/packages/@uppy/provider-views/src/utils/PartialTreeUtils/fill.ts b/packages/@uppy/provider-views/src/utils/PartialTreeUtils/afterFill.ts similarity index 95% rename from packages/@uppy/provider-views/src/utils/PartialTreeUtils/fill.ts rename to packages/@uppy/provider-views/src/utils/PartialTreeUtils/afterFill.ts index b4b67a96a1..721bce2732 100644 --- a/packages/@uppy/provider-views/src/utils/PartialTreeUtils/fill.ts +++ b/packages/@uppy/provider-views/src/utils/PartialTreeUtils/afterFill.ts @@ -52,7 +52,7 @@ const recursivelyFetch = async (queue: PQueue, poorTree: PartialTree, poorFolder }) } -const fill = async (partialTree: PartialTree, apiList: ApiList) : Promise => { +const afterFill = async (partialTree: PartialTree, apiList: ApiList) : Promise => { const queue = new PQueue({ concurrency: 6 }) // fill up the missing parts of a partialTree! @@ -76,4 +76,4 @@ const fill = async (partialTree: PartialTree, apiList: ApiList) : Promise const getFile = (tree: PartialTree, id: string) => tree.find((i) => i.id === id) as PartialTreeFile -describe('afterToggleCheckbox()', () => { - const oldPartialTree : PartialTree = [ - _root('ourRoot'), - _folder('1', { parentId: 'ourRoot' }), - _folder('2', { parentId: 'ourRoot' }), - _file('2_1', { parentId: '2' }), - _file('2_2', { parentId: '2' }), - _file('2_3', { parentId: '2' }), - _folder('2_4', { parentId: '2' }), // click - _file('2_4_1', { parentId: '2_4' }), - _file('2_4_2', { parentId: '2_4' }), - _file('2_4_3', { parentId: '2_4' }), - _file('3', { parentId: 'ourRoot' }), - _file('4', { parentId: 'ourRoot' }), - ] - - it('check folder: percolates up and down', () => { - const newTree = afterToggleCheckbox(oldPartialTree, ['2_4'], () => null) - - expect(getFolder(newTree, '2_4').status).toEqual('checked') - // percolates down - expect(getFile(newTree, '2_4_1').status).toEqual('checked') - expect(getFile(newTree, '2_4_2').status).toEqual('checked') - expect(getFile(newTree, '2_4_3').status).toEqual('checked') - // percolates up - expect(getFolder(newTree, '2').status).toEqual('partial') - }) - - it('uncheck folder: percolates up and down', () => { - const treeAfterClick1 = afterToggleCheckbox(oldPartialTree, ['2_4'], () => null) - - const tree = afterToggleCheckbox(treeAfterClick1, ['2_4'], () => null) - - expect(getFolder(tree, '2_4').status).toEqual('unchecked') - // percolates down - expect(getFile(tree, '2_4_1').status).toEqual('unchecked') - expect(getFile(tree, '2_4_2').status).toEqual('unchecked') - expect(getFile(tree, '2_4_3').status).toEqual('unchecked') - // percolates up - expect(getFolder(tree, '2').status).toEqual('unchecked') - }) - - it('gradually check all subfolders: marks parent folder as checked', () => { - const tree = afterToggleCheckbox(oldPartialTree, ['2_4_1', '2_4_2', '2_4_3'], () => null) - - // marks children as checked - expect(getFolder(tree, '2_4_1').status).toEqual('checked') - expect(getFolder(tree, '2_4_2').status).toEqual('checked') - expect(getFolder(tree, '2_4_3').status).toEqual('checked') - // marks parent folder as checked - expect(getFolder(tree, '2_4').status).toEqual('checked') - // marks parent parent folder as partially checked - expect(getFolder(tree, '2').status).toEqual('partial') - // and just randomly making sure unnrelated items didn't get checked - expect(getFile(tree, '3').status).toEqual('unchecked') - expect(getFile(tree, '2_2').status).toEqual('unchecked') - }) - - it('clicking partial folder: partial => checked => unchecked', () => { - // 1. click on 2_4_1, thus making 2_4 "partial" - const tree_1 = afterToggleCheckbox(oldPartialTree, ['2_4_1'], () => null) - - expect(getFolder(tree_1, '2_4').status).toEqual('partial') - // and test children while we're at it - expect(getFolder(tree_1, '2_4_1').status).toEqual('checked') - - // 2. click on 2_4, thus making 2_4 "checked" - const tree_2 = afterToggleCheckbox(tree_1, ['2_4'], () => null) - - expect(getFolder(tree_2, '2_4').status).toEqual('checked') - // and test children while we're at it - expect(getFolder(tree_2, '2_4_1').status).toEqual('checked') - expect(getFolder(tree_2, '2_4_2').status).toEqual('checked') - expect(getFolder(tree_2, '2_4_3').status).toEqual('checked') - - // 3. click on 2_4, thus making 2_4 "unchecked" - const tree_3 = afterToggleCheckbox(tree_2, ['2_4'], () => null) - - expect(getFolder(tree_3, '2_4').status).toEqual('unchecked') - // and test children while we're at it - expect(getFolder(tree_3, '2_4_1').status).toEqual('unchecked') - expect(getFolder(tree_3, '2_4_2').status).toEqual('unchecked') - expect(getFolder(tree_3, '2_4_3').status).toEqual('unchecked') - }) - - it('old partialTree is NOT mutated', () => { - const oldPartialTreeCopy = JSON.parse(JSON.stringify(oldPartialTree)); - afterToggleCheckbox(oldPartialTree, ['2_4_1'], () => null); - expect(oldPartialTree).toEqual(oldPartialTreeCopy); - }) -}) - -describe('afterOpenFolder()', () => { - it('open "checked" folder - all discovered files are marked as "checked"', () => { - const oldPartialTree : PartialTree = [ - _root('ourRoot'), - _folder('1', { parentId: 'ourRoot' }), - _folder('2', { parentId: 'ourRoot', cached: false, status: 'checked' }), - ] - - const fakeCompanionFiles = [{ requestPath: '666', isFolder: true }, { requestPath: '777', isFolder: false }, { requestPath: '888', isFolder: false }] as CompanionFile[] - - const clickedFolder = oldPartialTree.find((f) => f.id === '2') as PartialTreeFolderNode - - const newTree = afterOpenFolder(oldPartialTree, fakeCompanionFiles, clickedFolder, () => null, null) - - expect(getFolder(newTree, '666').status).toEqual('checked') - expect(getFile(newTree, '777').status).toEqual('checked') - expect(getFile(newTree, '888').status).toEqual('checked') - }) - - it('open "unchecked" folder - all discovered files are marked as "unchecked"', () => { - const oldPartialTree : PartialTree = [ - _root('ourRoot'), - _folder('1', { parentId: 'ourRoot' }), - _folder('2', { parentId: 'ourRoot', cached: false, status: 'unchecked' }), - ] - - const fakeCompanionFiles = [{ requestPath: '666', isFolder: true }, { requestPath: '777', isFolder: false }, { requestPath: '888', isFolder: false }] as CompanionFile[] - - const clickedFolder = oldPartialTree.find((f) => f.id === '2') as PartialTreeFolderNode - - const newTree = afterOpenFolder(oldPartialTree, fakeCompanionFiles, clickedFolder, () => null, null) - - expect(getFolder(newTree, '666').status).toEqual('unchecked') - expect(getFile(newTree, '777').status).toEqual('unchecked') - expect(getFile(newTree, '888').status).toEqual('unchecked') - }) -}) - -describe('afterScrollFolder()', () => { - it('scroll "checked" folder - all discovered files are marked as "checked"', () => { - const oldPartialTree : PartialTree = [ - _root('ourRoot'), - _folder('1', { parentId: 'ourRoot' }), - _folder('2', { parentId: 'ourRoot', cached: true, status: 'checked' }), - _file('2_1', { parentId: '2' }), - _file('2_2', { parentId: '2' }), - _file('2_3', { parentId: '2' }), - ] - - const fakeCompanionFiles = [{ requestPath: '666', isFolder: true }, { requestPath: '777', isFolder: false }, { requestPath: '888', isFolder: false }] as CompanionFile[] - - const newTree = afterScrollFolder(oldPartialTree, '2', fakeCompanionFiles, null, () => null) - - expect(getFolder(newTree, '666').status).toEqual('checked') - expect(getFile(newTree, '777').status).toEqual('checked') - expect(getFile(newTree, '888').status).toEqual('checked') - }) - - it('scroll "checked" folder - all discovered files are marked as "unchecked"', () => { - const oldPartialTree : PartialTree = [ - _root('ourRoot'), - _folder('1', { parentId: 'ourRoot' }), - _folder('2', { parentId: 'ourRoot', cached: true, status: 'unchecked' }), - _file('2_1', { parentId: '2' }), - _file('2_2', { parentId: '2' }), - _file('2_3', { parentId: '2' }), - ] - - const fakeCompanionFiles = [{ requestPath: '666', isFolder: true }, { requestPath: '777', isFolder: false }, { requestPath: '888', isFolder: false }] as CompanionFile[] - - const newTree = afterScrollFolder(oldPartialTree, '2', fakeCompanionFiles, null, () => null) - - expect(getFolder(newTree, '666').status).toEqual('unchecked') - expect(getFile(newTree, '777').status).toEqual('unchecked') - expect(getFile(newTree, '888').status).toEqual('unchecked') - }) -}) - -describe('injectPaths()', () => { - // Note that this is a tree that doesn't require any api calls, everything is cached already - const tree : PartialTree = [ - _root('ourRoot'), - _folder('1', { parentId: 'ourRoot' }), - _folder('2', { parentId: 'ourRoot' }), - _file('2_1', { parentId: '2' }), - _file('2_2', { parentId: '2', status: 'checked' }), - _file('2_3', { parentId: '2' }), - _folder('2_4', { parentId: '2', status: 'checked' }), - _file('2_4_1', { parentId: '2_4', status: 'checked' }), - _file('2_4_2', { parentId: '2_4', status: 'checked' }), - _file('2_4_3', { parentId: '2_4', status: 'checked' }), - _file('3', { parentId: 'ourRoot' }), - _file('4', { parentId: 'ourRoot' }), - ] - const checkedFiles = tree.filter((item) => item.type === 'file' && item.status === 'checked') as PartialTreeFile[] - - // These test cases are based on documentation for .absolutePath and .relativePath (https://uppy.io/docs/uppy/#filemeta) - it('.absolutePath always begins with / + always ends with the file’s name.', () => { - const result = injectPaths(tree, checkedFiles) - - expect(result.find((f) => f.id === '2_2')!.absDirPath).toEqual('/name_2/name_2_2.jpg') - expect(result.find((f) => f.id === '2_4_3')!.absDirPath).toEqual('/name_2/name_2_4/name_2_4_3.jpg') - }) - - it('.relativePath is null when file is selected independently', () => { - const result = injectPaths(tree, checkedFiles) - - // .relDirPath should be `undefined`, which will make .relativePath `null` eventually - expect(result.find((f) => f.id === '2_2')!.relDirPath).toEqual(undefined) - }) - - it('.relativePath attends to highest checked folder', () => { - const result = injectPaths(tree, checkedFiles) - - expect(result.find((f) => f.id === '2_4_1')!.relDirPath).toEqual('name_2_4/name_2_4_1.jpg') - }) -}) - -describe('fill()', () => { +describe('afterFill()', () => { it('fetches an already loaded file', async () => { const tree : PartialTree = [ _root('ourRoot'), @@ -278,7 +68,7 @@ describe('fill()', () => { _file('4', { parentId: 'ourRoot' }), ] const mock = vi.fn() - const result = await fill(tree, mock) + const result = await afterFill(tree, mock) // While we're at it - make sure we're not doing excessive api calls! expect(mock.mock.calls.length).toEqual(0) @@ -303,7 +93,7 @@ describe('fill()', () => { } return Promise.reject() } - const result = await fill(tree, mock) + const result = await afterFill(tree, mock) expect(result.length).toEqual(4) expect(result.map((f) => f.id)).toEqual(['2_1', '2_2', '2_3', '2_4']) @@ -322,7 +112,7 @@ describe('fill()', () => { } return Promise.reject() } - const result = await fill(tree, mock) + const result = await afterFill(tree, mock) expect(result.length).toEqual(2) expect(result.map((f) => f.id)).toEqual(['111', '222']) @@ -346,7 +136,7 @@ describe('fill()', () => { } return Promise.reject() } - const result = await fill(tree, mock) + const result = await afterFill(tree, mock) expect(result.length).toEqual(5) expect(result.map((f) => f.id)).toEqual(['2_1', '2_2', '2_3', '666_1', '666_2']) @@ -388,13 +178,183 @@ describe('fill()', () => { } return Promise.reject() } - const result = await fill(tree, mock) + const result = await afterFill(tree, mock) expect(result.length).toEqual(8) expect(result.map((f) => f.id)).toEqual(['2_1', '2_2', '3_1', '2_3', '666_1', '777_1', '777_2_1', '777_2_1_1']) }) }) +describe('afterOpenFolder()', () => { + it('open "checked" folder - all discovered files are marked as "checked"', () => { + const oldPartialTree : PartialTree = [ + _root('ourRoot'), + _folder('1', { parentId: 'ourRoot' }), + _folder('2', { parentId: 'ourRoot', cached: false, status: 'checked' }), + ] + + const fakeCompanionFiles = [{ requestPath: '666', isFolder: true }, { requestPath: '777', isFolder: false }, { requestPath: '888', isFolder: false }] as CompanionFile[] + + const clickedFolder = oldPartialTree.find((f) => f.id === '2') as PartialTreeFolderNode + + const newTree = afterOpenFolder(oldPartialTree, fakeCompanionFiles, clickedFolder, () => null, null) + + expect(getFolder(newTree, '666').status).toEqual('checked') + expect(getFile(newTree, '777').status).toEqual('checked') + expect(getFile(newTree, '888').status).toEqual('checked') + }) + + it('open "unchecked" folder - all discovered files are marked as "unchecked"', () => { + const oldPartialTree : PartialTree = [ + _root('ourRoot'), + _folder('1', { parentId: 'ourRoot' }), + _folder('2', { parentId: 'ourRoot', cached: false, status: 'unchecked' }), + ] + + const fakeCompanionFiles = [{ requestPath: '666', isFolder: true }, { requestPath: '777', isFolder: false }, { requestPath: '888', isFolder: false }] as CompanionFile[] + + const clickedFolder = oldPartialTree.find((f) => f.id === '2') as PartialTreeFolderNode + + const newTree = afterOpenFolder(oldPartialTree, fakeCompanionFiles, clickedFolder, () => null, null) + + expect(getFolder(newTree, '666').status).toEqual('unchecked') + expect(getFile(newTree, '777').status).toEqual('unchecked') + expect(getFile(newTree, '888').status).toEqual('unchecked') + }) +}) + +describe('afterScrollFolder()', () => { + it('scroll "checked" folder - all discovered files are marked as "checked"', () => { + const oldPartialTree : PartialTree = [ + _root('ourRoot'), + _folder('1', { parentId: 'ourRoot' }), + _folder('2', { parentId: 'ourRoot', cached: true, status: 'checked' }), + _file('2_1', { parentId: '2' }), + _file('2_2', { parentId: '2' }), + _file('2_3', { parentId: '2' }), + ] + + const fakeCompanionFiles = [{ requestPath: '666', isFolder: true }, { requestPath: '777', isFolder: false }, { requestPath: '888', isFolder: false }] as CompanionFile[] + + const newTree = afterScrollFolder(oldPartialTree, '2', fakeCompanionFiles, null, () => null) + + expect(getFolder(newTree, '666').status).toEqual('checked') + expect(getFile(newTree, '777').status).toEqual('checked') + expect(getFile(newTree, '888').status).toEqual('checked') + }) + + it('scroll "checked" folder - all discovered files are marked as "unchecked"', () => { + const oldPartialTree : PartialTree = [ + _root('ourRoot'), + _folder('1', { parentId: 'ourRoot' }), + _folder('2', { parentId: 'ourRoot', cached: true, status: 'unchecked' }), + _file('2_1', { parentId: '2' }), + _file('2_2', { parentId: '2' }), + _file('2_3', { parentId: '2' }), + ] + + const fakeCompanionFiles = [{ requestPath: '666', isFolder: true }, { requestPath: '777', isFolder: false }, { requestPath: '888', isFolder: false }] as CompanionFile[] + + const newTree = afterScrollFolder(oldPartialTree, '2', fakeCompanionFiles, null, () => null) + + expect(getFolder(newTree, '666').status).toEqual('unchecked') + expect(getFile(newTree, '777').status).toEqual('unchecked') + expect(getFile(newTree, '888').status).toEqual('unchecked') + }) +}) + +describe('afterToggleCheckbox()', () => { + const oldPartialTree : PartialTree = [ + _root('ourRoot'), + _folder('1', { parentId: 'ourRoot' }), + _folder('2', { parentId: 'ourRoot' }), + _file('2_1', { parentId: '2' }), + _file('2_2', { parentId: '2' }), + _file('2_3', { parentId: '2' }), + _folder('2_4', { parentId: '2' }), // click + _file('2_4_1', { parentId: '2_4' }), + _file('2_4_2', { parentId: '2_4' }), + _file('2_4_3', { parentId: '2_4' }), + _file('3', { parentId: 'ourRoot' }), + _file('4', { parentId: 'ourRoot' }), + ] + + it('check folder: percolates up and down', () => { + const newTree = afterToggleCheckbox(oldPartialTree, ['2_4'], () => null) + + expect(getFolder(newTree, '2_4').status).toEqual('checked') + // percolates down + expect(getFile(newTree, '2_4_1').status).toEqual('checked') + expect(getFile(newTree, '2_4_2').status).toEqual('checked') + expect(getFile(newTree, '2_4_3').status).toEqual('checked') + // percolates up + expect(getFolder(newTree, '2').status).toEqual('partial') + }) + + it('uncheck folder: percolates up and down', () => { + const treeAfterClick1 = afterToggleCheckbox(oldPartialTree, ['2_4'], () => null) + + const tree = afterToggleCheckbox(treeAfterClick1, ['2_4'], () => null) + + expect(getFolder(tree, '2_4').status).toEqual('unchecked') + // percolates down + expect(getFile(tree, '2_4_1').status).toEqual('unchecked') + expect(getFile(tree, '2_4_2').status).toEqual('unchecked') + expect(getFile(tree, '2_4_3').status).toEqual('unchecked') + // percolates up + expect(getFolder(tree, '2').status).toEqual('unchecked') + }) + + it('gradually check all subfolders: marks parent folder as checked', () => { + const tree = afterToggleCheckbox(oldPartialTree, ['2_4_1', '2_4_2', '2_4_3'], () => null) + + // marks children as checked + expect(getFolder(tree, '2_4_1').status).toEqual('checked') + expect(getFolder(tree, '2_4_2').status).toEqual('checked') + expect(getFolder(tree, '2_4_3').status).toEqual('checked') + // marks parent folder as checked + expect(getFolder(tree, '2_4').status).toEqual('checked') + // marks parent parent folder as partially checked + expect(getFolder(tree, '2').status).toEqual('partial') + // and just randomly making sure unnrelated items didn't get checked + expect(getFile(tree, '3').status).toEqual('unchecked') + expect(getFile(tree, '2_2').status).toEqual('unchecked') + }) + + it('clicking partial folder: partial => checked => unchecked', () => { + // 1. click on 2_4_1, thus making 2_4 "partial" + const tree_1 = afterToggleCheckbox(oldPartialTree, ['2_4_1'], () => null) + + expect(getFolder(tree_1, '2_4').status).toEqual('partial') + // and test children while we're at it + expect(getFolder(tree_1, '2_4_1').status).toEqual('checked') + + // 2. click on 2_4, thus making 2_4 "checked" + const tree_2 = afterToggleCheckbox(tree_1, ['2_4'], () => null) + + expect(getFolder(tree_2, '2_4').status).toEqual('checked') + // and test children while we're at it + expect(getFolder(tree_2, '2_4_1').status).toEqual('checked') + expect(getFolder(tree_2, '2_4_2').status).toEqual('checked') + expect(getFolder(tree_2, '2_4_3').status).toEqual('checked') + + // 3. click on 2_4, thus making 2_4 "unchecked" + const tree_3 = afterToggleCheckbox(tree_2, ['2_4'], () => null) + + expect(getFolder(tree_3, '2_4').status).toEqual('unchecked') + // and test children while we're at it + expect(getFolder(tree_3, '2_4_1').status).toEqual('unchecked') + expect(getFolder(tree_3, '2_4_2').status).toEqual('unchecked') + expect(getFolder(tree_3, '2_4_3').status).toEqual('unchecked') + }) + + it('old partialTree is NOT mutated', () => { + const oldPartialTreeCopy = JSON.parse(JSON.stringify(oldPartialTree)); + afterToggleCheckbox(oldPartialTree, ['2_4_1'], () => null); + expect(oldPartialTree).toEqual(oldPartialTreeCopy); + }) +}) + describe('getNOfSelectedFiles()', () => { it('gets all leaf items', () => { const tree : PartialTree = [ @@ -424,3 +384,43 @@ describe('getNOfSelectedFiles()', () => { expect(result).toEqual(1) }) }) + +describe('injectPaths()', () => { + // Note that this is a tree that doesn't require any api calls, everything is cached already + const tree : PartialTree = [ + _root('ourRoot'), + _folder('1', { parentId: 'ourRoot' }), + _folder('2', { parentId: 'ourRoot' }), + _file('2_1', { parentId: '2' }), + _file('2_2', { parentId: '2', status: 'checked' }), + _file('2_3', { parentId: '2' }), + _folder('2_4', { parentId: '2', status: 'checked' }), + _file('2_4_1', { parentId: '2_4', status: 'checked' }), + _file('2_4_2', { parentId: '2_4', status: 'checked' }), + _file('2_4_3', { parentId: '2_4', status: 'checked' }), + _file('3', { parentId: 'ourRoot' }), + _file('4', { parentId: 'ourRoot' }), + ] + const checkedFiles = tree.filter((item) => item.type === 'file' && item.status === 'checked') as PartialTreeFile[] + + // These test cases are based on documentation for .absolutePath and .relativePath (https://uppy.io/docs/uppy/#filemeta) + it('.absolutePath always begins with / + always ends with the file’s name.', () => { + const result = injectPaths(tree, checkedFiles) + + expect(result.find((f) => f.id === '2_2')!.absDirPath).toEqual('/name_2/name_2_2.jpg') + expect(result.find((f) => f.id === '2_4_3')!.absDirPath).toEqual('/name_2/name_2_4/name_2_4_3.jpg') + }) + + it('.relativePath is null when file is selected independently', () => { + const result = injectPaths(tree, checkedFiles) + + // .relDirPath should be `undefined`, which will make .relativePath `null` eventually + expect(result.find((f) => f.id === '2_2')!.relDirPath).toEqual(undefined) + }) + + it('.relativePath attends to highest checked folder', () => { + const result = injectPaths(tree, checkedFiles) + + expect(result.find((f) => f.id === '2_4_1')!.relDirPath).toEqual('name_2_4/name_2_4_1.jpg') + }) +}) diff --git a/packages/@uppy/provider-views/src/utils/PartialTreeUtils/index.ts b/packages/@uppy/provider-views/src/utils/PartialTreeUtils/index.ts index 7b50f5f27f..52bfec8d56 100644 --- a/packages/@uppy/provider-views/src/utils/PartialTreeUtils/index.ts +++ b/packages/@uppy/provider-views/src/utils/PartialTreeUtils/index.ts @@ -1,11 +1,11 @@ import afterOpenFolder from './afterOpenFolder' import afterScrollFolder from './afterScrollFolder' import afterToggleCheckbox from './afterToggleCheckbox' -import fill from './fill' +import afterFill from './afterFill' export default { afterOpenFolder, afterScrollFolder, afterToggleCheckbox, - fill + afterFill } From f505d5eb1c93aecc55af06a9f37cff5f3ab7e025 Mon Sep 17 00:00:00 2001 From: Evgenia Karunus Date: Wed, 1 May 2024 07:03:28 +0400 Subject: [PATCH 104/170] `donePicking()` - superseded a notification to i18n one --- .../@uppy/provider-views/src/ProviderView/ProviderView.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx b/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx index b3b255eadc..a91de9f006 100644 --- a/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx +++ b/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx @@ -303,13 +303,16 @@ export default class ProviderView{ }) if (filesToAdd.length > 0) { - this.plugin.uppy.info(`${filesToAdd.length} files added`) + // TODO I don't think we need to be showing this - we don't show this info when we're dropping files e.g. + this.plugin.uppy.info( + this.plugin.uppy.i18n('addedNumFiles', { numFiles: filesToAdd.length }) + ) } if (filesAlreadyAdded.length > 0) { this.plugin.uppy.info(`Not adding ${filesAlreadyAdded.length} files because they already exist`) } if (filesNotPassingRestrictions.length > 0) { - this.plugin.uppy.info(`Not adding ${filesNotPassingRestrictions.length} files they didn't pass restrictions`) + this.plugin.uppy.info(`Not adding ${filesNotPassingRestrictions.length} files because they didn't pass restrictions`) } this.plugin.uppy.addFiles(filesToAdd) }).catch(handleError(this.plugin.uppy)) From 925cdf90b08bf8698efe3a7f688dd54c54829991 Mon Sep 17 00:00:00 2001 From: Evgenia Karunus Date: Thu, 2 May 2024 05:30:07 +0400 Subject: [PATCH 105/170] GoogleDrive - make the shared drive checkable --- .../google-drive/src/DriveProviderViews.ts | 18 ------------ .../@uppy/google-drive/src/GoogleDrive.tsx | 3 +- .../src/Item/components/ListLi.tsx | 28 ++++++++----------- .../@uppy/provider-views/src/Item/index.tsx | 4 --- 4 files changed, 13 insertions(+), 40 deletions(-) delete mode 100644 packages/@uppy/google-drive/src/DriveProviderViews.ts diff --git a/packages/@uppy/google-drive/src/DriveProviderViews.ts b/packages/@uppy/google-drive/src/DriveProviderViews.ts deleted file mode 100644 index 27f37a3788..0000000000 --- a/packages/@uppy/google-drive/src/DriveProviderViews.ts +++ /dev/null @@ -1,18 +0,0 @@ -import type { PartialTreeFile, PartialTreeFolderNode } from '@uppy/core/lib/Uppy' -import { ProviderViews } from '@uppy/provider-views' -import type { Body, Meta } from '@uppy/utils/lib/UppyFile' - -export default class DriveProviderViews< - M extends Meta, - B extends Body, -> extends ProviderViews { - toggleCheckbox = (e: Event, file: PartialTreeFile | PartialTreeFolderNode, isShiftKeyPressed: boolean): void => { - e.stopPropagation() - e.preventDefault() - - // Shared Drives aren't selectable; for all else, defer to the base ProviderView. - if (!file.data.custom!.isSharedDrive) { - super.toggleCheckbox(e, file, isShiftKeyPressed) - } - } -} diff --git a/packages/@uppy/google-drive/src/GoogleDrive.tsx b/packages/@uppy/google-drive/src/GoogleDrive.tsx index c1b8e16e74..3ecf440979 100644 --- a/packages/@uppy/google-drive/src/GoogleDrive.tsx +++ b/packages/@uppy/google-drive/src/GoogleDrive.tsx @@ -10,7 +10,6 @@ import { h, type ComponentChild } from 'preact' import type { UppyFile, Body, Meta } from '@uppy/utils/lib/UppyFile' import type { UnknownProviderPluginState } from '@uppy/core/lib/Uppy.ts' -import DriveProviderViews from './DriveProviderViews.ts' import locale from './locale.ts' // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore We don't want TS to generate types for the package.json @@ -103,7 +102,7 @@ export default class GoogleDrive< } install(): void { - this.view = new DriveProviderViews(this, { + this.view = new ProviderViews(this, { provider: this.provider, loadAllFiles: false, }) diff --git a/packages/@uppy/provider-views/src/Item/components/ListLi.tsx b/packages/@uppy/provider-views/src/Item/components/ListLi.tsx index f0b3013a65..9379ea5669 100644 --- a/packages/@uppy/provider-views/src/Item/components/ListLi.tsx +++ b/packages/@uppy/provider-views/src/Item/components/ListLi.tsx @@ -15,7 +15,6 @@ type ListItemProps = { className: string isDisabled: boolean restrictionError?: RestrictionError | null - isCheckboxDisabled: boolean status: PartialTreeStatus toggleCheckbox: (event: Event) => void type: string @@ -34,7 +33,6 @@ export default function ListItem( className, isDisabled, restrictionError, - isCheckboxDisabled, status, toggleCheckbox, type, @@ -51,20 +49,18 @@ export default function ListItem( className={className} title={isDisabled ? restrictionError?.message : undefined} > - {!isCheckboxDisabled ? - - name="listitem" - id={id} - checked={status === "checked" ? true : false} - aria-label={type === 'file' ? null : i18n('allFilesFromFolderNamed', { name: title })} - disabled={isDisabled} - data-uppy-super-focusable - /> - : null} + + name="listitem" + id={id} + checked={status === "checked" ? true : false} + aria-label={type === 'file' ? null : i18n('allFilesFromFolderNamed', { name: title })} + disabled={isDisabled} + data-uppy-super-focusable + /> { type === 'file' ? diff --git a/packages/@uppy/provider-views/src/Item/index.tsx b/packages/@uppy/provider-views/src/Item/index.tsx index 6faf056b77..2cde8ec827 100644 --- a/packages/@uppy/provider-views/src/Item/index.tsx +++ b/packages/@uppy/provider-views/src/Item/index.tsx @@ -11,8 +11,6 @@ import type { PartialTreeFile, PartialTreeFolderNode, PartialTreeId, Uppy } from import type { RestrictionError } from '@uppy/core/lib/Restricter.ts' import type { CompanionFile } from '@uppy/utils/lib/CompanionFile' -const VIRTUAL_SHARED_DIR = 'shared-with-me' - type ItemProps = { viewType: string toggleCheckbox: (event: Event) => void @@ -56,12 +54,10 @@ export default function Item( { ...sharedProps, type: 'folder', - isCheckboxDisabled: file.id === VIRTUAL_SHARED_DIR, handleFolderClick: () => openFolder(file.id), } : { ...sharedProps, - isCheckboxDisabled: false, type: 'file' } From ceb52ca79af35adf549afb968f839526899ccb5c Mon Sep 17 00:00:00 2001 From: Evgenia Karunus Date: Thu, 2 May 2024 08:35:43 +0400 Subject: [PATCH 106/170] `Item.tsx` - standardize names; remove unnecessary question marks from props --- .../Item/components/{GridLi.tsx => GridItem.tsx} | 15 +++++++-------- .../Item/components/{ListLi.tsx => ListItem.tsx} | 2 +- packages/@uppy/provider-views/src/Item/index.tsx | 16 +++++++--------- 3 files changed, 15 insertions(+), 18 deletions(-) rename packages/@uppy/provider-views/src/Item/components/{GridLi.tsx => GridItem.tsx} (81%) rename packages/@uppy/provider-views/src/Item/components/{ListLi.tsx => ListItem.tsx} (97%) diff --git a/packages/@uppy/provider-views/src/Item/components/GridLi.tsx b/packages/@uppy/provider-views/src/Item/components/GridItem.tsx similarity index 81% rename from packages/@uppy/provider-views/src/Item/components/GridLi.tsx rename to packages/@uppy/provider-views/src/Item/components/GridItem.tsx index 1d4a2e5d0b..cc708be433 100644 --- a/packages/@uppy/provider-views/src/Item/components/GridLi.tsx +++ b/packages/@uppy/provider-views/src/Item/components/GridItem.tsx @@ -1,25 +1,24 @@ /* eslint-disable react/require-default-props */ import { h } from 'preact' -import classNames from 'classnames' import type { RestrictionError } from '@uppy/core/lib/Restricter' import type { Body, Meta } from '@uppy/utils/lib/UppyFile' import type { PartialTreeStatus } from '@uppy/core/lib/Uppy' -type GridListItemProps = { +type GridItemProps = { className: string isDisabled: boolean - restrictionError?: RestrictionError | null + restrictionError: RestrictionError | null status: PartialTreeStatus - title?: string + title: string itemIconEl: any - showTitles?: boolean + showTitles: boolean toggleCheckbox: (event: Event) => void id: string children?: JSX.Element } -function GridListItem( - props: GridListItemProps, +function GridItem( + props: GridItemProps, ): h.JSX.Element { const { className, @@ -62,4 +61,4 @@ function GridListItem( ) } -export default GridListItem +export default GridItem diff --git a/packages/@uppy/provider-views/src/Item/components/ListLi.tsx b/packages/@uppy/provider-views/src/Item/components/ListItem.tsx similarity index 97% rename from packages/@uppy/provider-views/src/Item/components/ListLi.tsx rename to packages/@uppy/provider-views/src/Item/components/ListItem.tsx index 9379ea5669..2a367460f7 100644 --- a/packages/@uppy/provider-views/src/Item/components/ListLi.tsx +++ b/packages/@uppy/provider-views/src/Item/components/ListItem.tsx @@ -14,7 +14,7 @@ import { h } from 'preact' type ListItemProps = { className: string isDisabled: boolean - restrictionError?: RestrictionError | null + restrictionError: RestrictionError | null status: PartialTreeStatus toggleCheckbox: (event: Event) => void type: string diff --git a/packages/@uppy/provider-views/src/Item/index.tsx b/packages/@uppy/provider-views/src/Item/index.tsx index 2cde8ec827..e115ae51fd 100644 --- a/packages/@uppy/provider-views/src/Item/index.tsx +++ b/packages/@uppy/provider-views/src/Item/index.tsx @@ -5,9 +5,9 @@ import classNames from 'classnames' import type { I18n } from '@uppy/utils/lib/Translator' import type { Meta, Body } from '@uppy/utils/lib/UppyFile' import ItemIcon from './components/ItemIcon.tsx' -import GridListItem from './components/GridLi.tsx' -import ListItem from './components/ListLi.tsx' -import type { PartialTreeFile, PartialTreeFolderNode, PartialTreeId, Uppy } from '@uppy/core/lib/Uppy.ts' +import GridItem from './components/GridItem.tsx' +import ListItem from './components/ListItem.tsx' +import type { PartialTreeFile, PartialTreeFolderNode, PartialTreeId } from '@uppy/core/lib/Uppy.ts' import type { RestrictionError } from '@uppy/core/lib/Restricter.ts' import type { CompanionFile } from '@uppy/utils/lib/CompanionFile' @@ -63,14 +63,12 @@ export default function Item( switch (viewType) { case 'grid': - return {...ourProps} /> + return {...ourProps} /> case 'list': - return ( - {...ourProps} /> - ) + return {...ourProps} /> case 'unsplash': return ( - {...ourProps} > + {...ourProps} > ( > {file.data.author!.name} - + ) default: throw new Error(`There is no such type ${viewType}`) From af35dbbe975983b1bec98d6e85a939fcb0b26f09 Mon Sep 17 00:00:00 2001 From: Evgenia Karunus Date: Thu, 2 May 2024 09:11:27 +0400 Subject: [PATCH 107/170] ProviderView.tsx - clicking "Cancel" should make all files "unchecked" --- .../src/ProviderView/ProviderView.tsx | 20 +++++++++---------- .../SearchProviderView/SearchProviderView.tsx | 17 ++++++++-------- 2 files changed, 18 insertions(+), 19 deletions(-) diff --git a/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx b/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx index a91de9f006..edc99cbee9 100644 --- a/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx +++ b/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx @@ -9,6 +9,7 @@ import type { PartialTreeFile, UnknownProviderPluginState, PartialTreeId, + PartialTree, } from '@uppy/core/lib/Uppy.ts' import type { Body, Meta, TagFile } from '@uppy/utils/lib/UppyFile' import type { CompanionFile } from '@uppy/utils/lib/CompanionFile.ts' @@ -52,7 +53,7 @@ const getDefaultState = (rootFolderId: string | null) : UnknownProviderPluginSta nextPagePath: null } ], - currentFolderId: null, + currentFolderId: rootFolderId, searchString: '', didFirstRender: false, username: null, @@ -115,7 +116,7 @@ export default class ProviderView{ this.resetPluginState = this.resetPluginState.bind(this) this.donePicking = this.donePicking.bind(this) this.render = this.render.bind(this) - this.cancelPicking = this.cancelPicking.bind(this) + this.cancelSelection = this.cancelSelection.bind(this) this.toggleCheckbox = this.toggleCheckbox.bind(this) // Set default state for the plugin @@ -139,13 +140,12 @@ export default class ProviderView{ this.plugin.setPluginState({ loading }) } - cancelPicking(): void { - const dashboard = this.plugin.uppy.getPlugin('Dashboard') - if (dashboard) { - // @ts-expect-error impossible to type this correctly without adding dashboard - // as a dependency to this package. - dashboard.hideAllPanels() - } + cancelSelection(): void { + const { partialTree } = this.plugin.getPluginState() + const newPartialTree : PartialTree = partialTree.map((item) => + item.type === 'root' ? item : { ...item, status: 'unchecked' } + ) + this.plugin.setPluginState({ partialTree: newPartialTree }) } #abortController: AbortController | undefined @@ -414,7 +414,7 @@ export default class ProviderView{ noResultsLabel={i18n('noFilesFound')} handleScroll={this.handleScroll} done={this.donePicking} - cancel={this.cancelPicking} + cancel={this.cancelSelection} headerComponent={ showBreadcrumbs={opts.showBreadcrumbs} diff --git a/packages/@uppy/provider-views/src/SearchProviderView/SearchProviderView.tsx b/packages/@uppy/provider-views/src/SearchProviderView/SearchProviderView.tsx index 19bfad7e13..39e55fb5b3 100644 --- a/packages/@uppy/provider-views/src/SearchProviderView/SearchProviderView.tsx +++ b/packages/@uppy/provider-views/src/SearchProviderView/SearchProviderView.tsx @@ -83,7 +83,7 @@ export default class SearchProviderView { this.resetPluginState = this.resetPluginState.bind(this) this.handleScroll = this.handleScroll.bind(this) this.donePicking = this.donePicking.bind(this) - this.cancelPicking = this.cancelPicking.bind(this) + this.cancelSelection = this.cancelSelection.bind(this) this.toggleCheckbox = this.toggleCheckbox.bind(this) this.render = this.render.bind(this) @@ -110,13 +110,12 @@ export default class SearchProviderView { this.plugin.setPluginState(defaultState) } - cancelPicking(): void { - const dashboard = this.plugin.uppy.getPlugin('Dashboard') - if (dashboard) { - // @ts-expect-error impossible to type this correctly without adding dashboard - // as a dependency to this package. - dashboard.hideAllPanels() - } + cancelSelection(): void { + const { partialTree } = this.plugin.getPluginState() + const newPartialTree : PartialTree = partialTree.map((item) => + item.type === 'root' ? item : { ...item, status: 'unchecked' } + ) + this.plugin.setPluginState({ partialTree: newPartialTree }) } async search(): Promise { @@ -260,7 +259,7 @@ export default class SearchProviderView { nOfSelectedFiles={getNOfSelectedFiles(partialTree)} handleScroll={this.handleScroll} done={this.donePicking} - cancel={this.cancelPicking} + cancel={this.cancelSelection} openFolder={() => {}} showSearchFilter={opts.showFilter} searchString={searchString} From 524e189cbde61b17b8e1146ec9fbe9a408c5ef3c Mon Sep 17 00:00:00 2001 From: Evgenia Karunus Date: Tue, 7 May 2024 16:27:26 +0400 Subject: [PATCH 108/170] everywhere - move `document.getSelection()?.removeAllRanges()` to to avoid repetition --- packages/@uppy/provider-views/src/Browser.tsx | 46 +++++++++---------- .../src/Item/components/ListItem.tsx | 1 - .../src/ProviderView/ProviderView.tsx | 10 +--- .../SearchProviderView/SearchProviderView.tsx | 10 +--- 4 files changed, 27 insertions(+), 40 deletions(-) diff --git a/packages/@uppy/provider-views/src/Browser.tsx b/packages/@uppy/provider-views/src/Browser.tsx index 0da8647805..bde30cbf57 100644 --- a/packages/@uppy/provider-views/src/Browser.tsx +++ b/packages/@uppy/provider-views/src/Browser.tsx @@ -14,6 +14,7 @@ import type { PartialTree, PartialTreeFile, PartialTreeFolderNode } from '@uppy/ import type { RestrictionError } from '@uppy/core/lib/Restricter.ts' import type { CompanionFile } from '@uppy/utils/lib/CompanionFile' import { useEffect, useState } from 'preact/hooks' +import type ProviderView from './ProviderView/ProviderView.tsx' type BrowserProps = { displayedPartialTree: (PartialTreeFile | PartialTreeFolderNode)[], @@ -21,7 +22,7 @@ type BrowserProps = { viewType: string headerComponent?: JSX.Element - toggleCheckbox: (event: Event, file: PartialTreeFile | PartialTreeFolderNode, isShiftKeyPressed: boolean) => void + toggleCheckbox: ProviderView['toggleCheckbox'] handleScroll: (event: Event) => Promise showTitles: boolean i18n: I18n @@ -88,6 +89,25 @@ function Browser( } }, []) + const renderItem = (item: PartialTreeFile | PartialTreeFolderNode) => ( + { + event.stopPropagation() + event.preventDefault() + // Prevent shift-clicking from highlighting file names + // (https://stackoverflow.com/a/1527797/3192470) + document.getSelection()?.removeAllRanges() + toggleCheckbox(item, isShiftKeyPressed) + }} + showTitles={showTitles} + i18n={i18n} + validateRestrictions={validateRestrictions} + openFolder={openFolder} + file={item} + /> + ) + return (
            (
              ( - toggleCheckbox(event, file, isShiftKeyPressed)} - showTitles={showTitles} - i18n={i18n} - validateRestrictions={validateRestrictions} - openFolder={openFolder} - file={file} - /> - )} + renderRow={renderItem} rowHeight={31} />
            @@ -155,17 +165,7 @@ function Browser( // making
              not focusable for firefox tabIndex={-1} > - {displayedPartialTree.map((file) => ( - toggleCheckbox(event, file, isShiftKeyPressed)} - showTitles={showTitles} - i18n={i18n} - validateRestrictions={validateRestrictions} - openFolder={openFolder} - file={file} - /> - ))} + {displayedPartialTree.map(renderItem)}
            ) diff --git a/packages/@uppy/provider-views/src/Item/components/ListItem.tsx b/packages/@uppy/provider-views/src/Item/components/ListItem.tsx index 2a367460f7..ae31bbd6cc 100644 --- a/packages/@uppy/provider-views/src/Item/components/ListItem.tsx +++ b/packages/@uppy/provider-views/src/Item/components/ListItem.tsx @@ -86,7 +86,6 @@ export default function ListItem(
            {showTitles && {title}} - }
          • ) diff --git a/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx b/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx index edc99cbee9..48e10d80fa 100644 --- a/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx +++ b/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx @@ -336,20 +336,14 @@ export default class ProviderView{ return breadcrumbs.toReversed() } - toggleCheckbox(e: Event, ourItem: PartialTreeFolderNode | PartialTreeFile, isShiftKeyPressed: boolean) { - e.stopPropagation() - e.preventDefault() - // Prevent shift-clicking from highlighting file names - // (https://stackoverflow.com/a/1527797/3192470) - document.getSelection()?.removeAllRanges() - + toggleCheckbox(ourItem: PartialTreeFolderNode | PartialTreeFile, isShiftKeyPressed: boolean) { const { partialTree } = this.plugin.getPluginState() const clickedRange = getClickedRange(ourItem.id, this.getDisplayedPartialTree(), isShiftKeyPressed, this.lastCheckbox) const newPartialTree = PartialTreeUtils.afterToggleCheckbox(partialTree, clickedRange, validateRestrictions(this.plugin)) this.plugin.setPluginState({ partialTree: newPartialTree }) - this.lastCheckbox = ourItem.id! + this.lastCheckbox = ourItem.id } getDisplayedPartialTree = () : (PartialTreeFile | PartialTreeFolderNode)[] => { diff --git a/packages/@uppy/provider-views/src/SearchProviderView/SearchProviderView.tsx b/packages/@uppy/provider-views/src/SearchProviderView/SearchProviderView.tsx index 39e55fb5b3..170c2d31d7 100644 --- a/packages/@uppy/provider-views/src/SearchProviderView/SearchProviderView.tsx +++ b/packages/@uppy/provider-views/src/SearchProviderView/SearchProviderView.tsx @@ -198,20 +198,14 @@ export default class SearchProviderView { this.resetPluginState() } - toggleCheckbox(e: Event, ourItem: PartialTreeFolderNode | PartialTreeFile, isShiftKeyPressed: boolean) { - e.stopPropagation() - e.preventDefault() - // Prevent shift-clicking from highlighting file names - // (https://stackoverflow.com/a/1527797/3192470) - document.getSelection()?.removeAllRanges() - + toggleCheckbox(ourItem: PartialTreeFolderNode | PartialTreeFile, isShiftKeyPressed: boolean) { const { partialTree } = this.plugin.getPluginState() const clickedRange = getClickedRange(ourItem.id, this.getDisplayedPartialTree(), isShiftKeyPressed, this.lastCheckbox) const newPartialTree = PartialTreeUtils.afterToggleCheckbox(partialTree, clickedRange, validateRestrictions(this.plugin)) this.plugin.setPluginState({ partialTree: newPartialTree }) - this.lastCheckbox = ourItem.id! + this.lastCheckbox = ourItem.id } getDisplayedPartialTree = () : (PartialTreeFile | PartialTreeFolderNode)[] => { From 7d5eee311869e00c9fbfb83c4d732fa836ea847c Mon Sep 17 00:00:00 2001 From: Evgenia Karunus Date: Wed, 8 May 2024 09:18:13 +0400 Subject: [PATCH 109/170] everywhere - standardize names and types of passed props --- packages/@uppy/provider-views/src/Browser.tsx | 21 +++++++++--------- .../provider-views/src/FooterActions.tsx | 22 ++++++++++--------- .../src/ProviderView/ProviderView.tsx | 4 ++-- .../SearchProviderView/SearchProviderView.tsx | 9 ++++---- 4 files changed, 29 insertions(+), 27 deletions(-) diff --git a/packages/@uppy/provider-views/src/Browser.tsx b/packages/@uppy/provider-views/src/Browser.tsx index bde30cbf57..d4d4de1454 100644 --- a/packages/@uppy/provider-views/src/Browser.tsx +++ b/packages/@uppy/provider-views/src/Browser.tsx @@ -10,7 +10,7 @@ import type { I18n } from '@uppy/utils/lib/Translator' import SearchFilterInput from './SearchFilterInput.tsx' import FooterActions from './FooterActions.tsx' import Item from './Item/index.tsx' -import type { PartialTree, PartialTreeFile, PartialTreeFolderNode } from '@uppy/core/lib/Uppy.ts' +import type { PartialTreeFile, PartialTreeFolderNode } from '@uppy/core/lib/Uppy.ts' import type { RestrictionError } from '@uppy/core/lib/Restricter.ts' import type { CompanionFile } from '@uppy/utils/lib/CompanionFile' import { useEffect, useState } from 'preact/hooks' @@ -19,7 +19,6 @@ import type ProviderView from './ProviderView/ProviderView.tsx' type BrowserProps = { displayedPartialTree: (PartialTreeFile | PartialTreeFolderNode)[], nOfSelectedFiles: number, - viewType: string headerComponent?: JSX.Element toggleCheckbox: ProviderView['toggleCheckbox'] @@ -34,11 +33,11 @@ type BrowserProps = { submitSearchString: () => void searchInputLabel: string clearSearchLabel: string - openFolder: (folderId: any) => void - cancel: () => void - done: () => void + openFolder: ProviderView['openFolder'] + cancelSelection: ProviderView['cancelSelection'] + donePicking: ProviderView['donePicking'] noResultsLabel: string - loadAllFiles?: boolean + loadAllFiles: boolean } function Browser( @@ -64,8 +63,8 @@ function Browser( searchInputLabel, clearSearchLabel, openFolder, - cancel, - done, + cancelSelection, + donePicking, noResultsLabel, loadAllFiles, } = props @@ -173,9 +172,9 @@ function Browser( {nOfSelectedFiles > 0 && ( )} diff --git a/packages/@uppy/provider-views/src/FooterActions.tsx b/packages/@uppy/provider-views/src/FooterActions.tsx index 37795de6b0..b6fc13f9b1 100644 --- a/packages/@uppy/provider-views/src/FooterActions.tsx +++ b/packages/@uppy/provider-views/src/FooterActions.tsx @@ -1,31 +1,33 @@ import { h } from 'preact' import type { I18n } from '@uppy/utils/lib/Translator' +import type ProviderView from './ProviderView' +import type { Meta, Body } from '@uppy/utils/lib/UppyFile' -export default function FooterActions({ - cancel, - done, +export default function FooterActions({ + cancelSelection, + donePicking, i18n, - selected, + nOfSelectedFiles, }: { - cancel: () => void - done: () => void + cancelSelection: ProviderView['cancelSelection'] + donePicking: ProviderView['donePicking'] i18n: I18n - selected: number + nOfSelectedFiles: number }): JSX.Element { return (
            + + { + aggregateRestrictionError && +
            + {aggregateRestrictionError} +
            + }
            ) } diff --git a/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx b/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx index 3c4fc8d34e..f62960ba0d 100644 --- a/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx +++ b/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx @@ -23,7 +23,6 @@ import Browser from '../Browser.tsx' import packageJson from '../../package.json' import PartialTreeUtils from '../utils/PartialTreeUtils' import getTagFile from '../utils/getTagFile.ts' -import getNOfSelectedFiles from '../utils/PartialTreeUtils/getNOfSelectedFiles.ts' import shouldHandleScroll from '../utils/shouldHandleScroll.ts' import handleError from '../utils/handleError.ts' import getClickedRange from '../utils/getClickedRange.ts' @@ -32,6 +31,7 @@ import SearchFilterInput from '../SearchFilterInput.tsx' import FooterActions from '../FooterActions.tsx' import type { ValidateableFile } from '@uppy/core/lib/Restricter.ts' import remoteFileObjToLocal from '@uppy/utils/lib/remoteFileObjToLocal' +import injectPaths from '../utils/PartialTreeUtils/injectPaths.ts' export function defaultPickerIcon(): JSX.Element { return ( @@ -284,27 +284,32 @@ export default class ProviderView{ async donePicking(): Promise { const { partialTree } = this.plugin.getPluginState() - this.setLoading(true) + this.setLoading(true) await this.#withAbort(async (signal) => { - const uppyFiles: CompanionFile[] = await PartialTreeUtils.afterFill( + const newPartialTree: PartialTree = await PartialTreeUtils.afterFill( partialTree, (path: PartialTreeId) => this.provider.list(path, { signal }), this.validateSingleFile ) + const checkedFiles = newPartialTree.filter((item) => item.type === 'file' && item.status === 'checked') as PartialTreeFile[] + const checkedFilesWithPaths = injectPaths(newPartialTree, checkedFiles) + const uppyFiles = checkedFilesWithPaths.map((file) => file.data) + + const aggregateRestrictionError = this.plugin.uppy.validateAggregateRestrictions(uppyFiles) + + if (aggregateRestrictionError) { + this.plugin.setPluginState({ partialTree: newPartialTree }) + return + } + const filesToAdd : TagFile[] = [] const filesAlreadyAdded : TagFile[] = [] - const filesNotPassingRestrictions : TagFile[] = [] uppyFiles.forEach((uppyFile) => { const tagFile = getTagFile(uppyFile, this.plugin.id, this.provider, this.plugin.opts.companionUrl) - if (validateRestrictions(this.plugin)(uppyFile)) { - filesNotPassingRestrictions.push(tagFile) - return - } - const id = getSafeFileId(tagFile) if (this.plugin.uppy.checkIfFileAlreadyExists(id)) { filesAlreadyAdded.push(tagFile) @@ -322,9 +327,6 @@ export default class ProviderView{ if (filesAlreadyAdded.length > 0) { this.plugin.uppy.info(`Not adding ${filesAlreadyAdded.length} files because they already exist`) } - if (filesNotPassingRestrictions.length > 0) { - this.plugin.uppy.info(`Not adding ${filesNotPassingRestrictions.length} files because they didn't pass restrictions`) - } this.plugin.uppy.addFiles(filesToAdd) }).catch(handleError(this.plugin.uppy)) @@ -398,8 +400,6 @@ export default class ProviderView{ ) } - const nOfSelectedFiles = getNOfSelectedFiles(partialTree) - return
            { isLoading={loading} /> - {nOfSelectedFiles > 0 && ( - - )} +
            } } diff --git a/packages/@uppy/provider-views/src/SearchProviderView/SearchProviderView.tsx b/packages/@uppy/provider-views/src/SearchProviderView/SearchProviderView.tsx index 81c0fc715b..65f227ecd0 100644 --- a/packages/@uppy/provider-views/src/SearchProviderView/SearchProviderView.tsx +++ b/packages/@uppy/provider-views/src/SearchProviderView/SearchProviderView.tsx @@ -192,8 +192,9 @@ export default class SearchProviderView { const { partialTree } = this.plugin.getPluginState() this.plugin.uppy.log('Adding remote search provider files') const checkedFiles = partialTree.filter((i) => i.type !== 'root' && i.status === 'checked') as PartialTreeFile[] - const withPaths = injectPaths(partialTree, checkedFiles) - const tagFiles = withPaths.map((file) => + const checkedFilesWithPaths = injectPaths(partialTree, checkedFiles) + const uppyFiles = checkedFilesWithPaths.map((file) => file.data) + const tagFiles = uppyFiles.map((file) => getTagFile(file, this.plugin.id, this.provider, this.plugin.opts.companionUrl) ) this.plugin.uppy.addFiles(tagFiles) @@ -255,8 +256,6 @@ export default class SearchProviderView { ) } - const nOfSelectedFiles = getNOfSelectedFiles(partialTree) - return
            { loadAllFiles={false} /> - {nOfSelectedFiles > 0 && ( - - )} +
            } } diff --git a/packages/@uppy/provider-views/src/utils/PartialTreeUtils/afterFill.ts b/packages/@uppy/provider-views/src/utils/PartialTreeUtils/afterFill.ts index c4282d5518..a372e00cd1 100644 --- a/packages/@uppy/provider-views/src/utils/PartialTreeUtils/afterFill.ts +++ b/packages/@uppy/provider-views/src/utils/PartialTreeUtils/afterFill.ts @@ -67,7 +67,7 @@ const afterFill = async ( partialTree: PartialTree, apiList: ApiList, validateSingleFile: (file: CompanionFile) => string | null, -) : Promise => { +) : Promise => { const queue = new PQueue({ concurrency: 6 }) // fill up the missing parts of a partialTree! @@ -85,10 +85,7 @@ const afterFill = async ( await queue.onIdle() - // Return all 'checked' files - const checkedFiles = poorTree.filter((item) => item.type === 'file' && item.status === 'checked') as PartialTreeFile[] - const uppyFiles = injectPaths(poorTree, checkedFiles) - return uppyFiles + return poorTree } export default afterFill diff --git a/packages/@uppy/provider-views/src/utils/PartialTreeUtils/injectPaths.ts b/packages/@uppy/provider-views/src/utils/PartialTreeUtils/injectPaths.ts index f9bb170d0f..c1b1712ed7 100644 --- a/packages/@uppy/provider-views/src/utils/PartialTreeUtils/injectPaths.ts +++ b/packages/@uppy/provider-views/src/utils/PartialTreeUtils/injectPaths.ts @@ -23,7 +23,7 @@ const getPath = ( } // See "Uppy file properties" documentation for `.absolutePath` and `.relativePath` (https://uppy.io/docs/uppy/#working-with-uppy-files) -const injectPaths = (partialTree: PartialTree, files: PartialTreeFile[]) : CompanionFile[] => { +const injectPaths = (partialTree: PartialTree, files: PartialTreeFile[]) : PartialTreeFile[] => { const cache : Cache = {} const injectedFiles = files.map((file) => { @@ -36,15 +36,18 @@ const injectPaths = (partialTree: PartialTree, files: PartialTreeFile[]) : Compa const absDirPath = '/' + absFolders.map((i) => i.data.name).join('/') const relDirPath = relFolders.length === 1 - // Must return null + // Must return `undefined` (which later turns into `null` in `.getTagFile()`) // (https://github.com/transloadit/uppy/pull/4537#issuecomment-1629136652) ? undefined : relFolders.map((i) => i.data.name).join('/') return { - ...file.data, - absDirPath, - relDirPath + ...file, + data: { + ...file.data, + absDirPath, + relDirPath + } } }) diff --git a/packages/@uppy/provider-views/src/utils/validateRestrictions.ts b/packages/@uppy/provider-views/src/utils/validateRestrictions.ts deleted file mode 100644 index 5668404e58..0000000000 --- a/packages/@uppy/provider-views/src/utils/validateRestrictions.ts +++ /dev/null @@ -1,22 +0,0 @@ -import type { RestrictionError } from "@uppy/core/lib/Restricter" -import type { PartialTreeFile, UnknownProviderPlugin, UnknownSearchProviderPlugin } from "@uppy/core/lib/Uppy" -import type { CompanionFile } from "@uppy/utils/lib/CompanionFile" -import remoteFileObjToLocal from "@uppy/utils/lib/remoteFileObjToLocal" - -const validateRestrictions = - (plugin: UnknownProviderPlugin | UnknownSearchProviderPlugin) => - (file: CompanionFile) - : RestrictionError | null => { - if (file.isFolder) return null - - const localData = remoteFileObjToLocal(file) - - const { partialTree } = plugin.getPluginState() - const aleadyAddedFiles = plugin.uppy.getFiles() - const checkedFiles = partialTree.filter((item) => item.type === 'file' && item.status === 'checked') as PartialTreeFile[] - const checkedFilesData = checkedFiles.map((item) => item.data) - - return plugin.uppy.validateRestrictions(localData, [...aleadyAddedFiles, ...checkedFilesData]) -} - -export default validateRestrictions From 21067a5ac65d34fef90d44ac1add7d693071c5fa Mon Sep 17 00:00:00 2001 From: Evgenia Karunus Date: Fri, 17 May 2024 22:56:12 +0400 Subject: [PATCH 114/170] SearchProvider, NormalProvider - unite the way we addFiles() Same notifications, same code, same everything --- .../provider-views/src/FooterActions.tsx | 11 +--- .../src/ProviderView/ProviderView.tsx | 58 ++++++++----------- .../SearchProviderView/SearchProviderView.tsx | 26 +++++---- .../src/utils/PartialTreeUtils/afterFill.ts | 1 - ...ctPaths.ts => getCheckedFilesWithPaths.ts} | 22 +++---- .../provider-views/src/utils/addFiles.ts | 30 ++++++++++ 6 files changed, 84 insertions(+), 64 deletions(-) rename packages/@uppy/provider-views/src/utils/PartialTreeUtils/{injectPaths.ts => getCheckedFilesWithPaths.ts} (76%) create mode 100644 packages/@uppy/provider-views/src/utils/addFiles.ts diff --git a/packages/@uppy/provider-views/src/FooterActions.tsx b/packages/@uppy/provider-views/src/FooterActions.tsx index f27d0a4c92..059e77d654 100644 --- a/packages/@uppy/provider-views/src/FooterActions.tsx +++ b/packages/@uppy/provider-views/src/FooterActions.tsx @@ -3,9 +3,8 @@ import type { I18n } from '@uppy/utils/lib/Translator' import type ProviderView from './ProviderView' import type { Meta, Body } from '@uppy/utils/lib/UppyFile' import classNames from 'classnames' -import type { PartialTree, PartialTreeFile } from '@uppy/core/lib/Uppy' +import type { PartialTree } from '@uppy/core/lib/Uppy' import getNOfSelectedFiles from './utils/PartialTreeUtils/getNOfSelectedFiles' -import type { ValidateableFile } from '@uppy/core/lib/Restricter' import { useMemo } from 'preact/hooks' export default function FooterActions({ @@ -19,14 +18,10 @@ export default function FooterActions({ donePicking: ProviderView['donePicking'] i18n: I18n partialTree: PartialTree - validateAggregateRestrictions: (addingFiles: ValidateableFile[]) => string | null + validateAggregateRestrictions: ProviderView['validateAggregateRestrictions'] }) { const aggregateRestrictionError = useMemo(() => { - const checkedFiles = partialTree.filter((item) => - item.type === 'file' && item.status === 'checked' - ) as PartialTreeFile[] - const uppyFiles = checkedFiles.map((file) => file.data) - return validateAggregateRestrictions(uppyFiles) + return validateAggregateRestrictions(partialTree) }, [partialTree]) const nOfSelectedFiles = useMemo(() => { diff --git a/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx b/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx index f62960ba0d..981d7e202e 100644 --- a/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx +++ b/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx @@ -31,7 +31,8 @@ import SearchFilterInput from '../SearchFilterInput.tsx' import FooterActions from '../FooterActions.tsx' import type { ValidateableFile } from '@uppy/core/lib/Restricter.ts' import remoteFileObjToLocal from '@uppy/utils/lib/remoteFileObjToLocal' -import injectPaths from '../utils/PartialTreeUtils/injectPaths.ts' +import addFiles from '../utils/addFiles.ts' +import getCheckedFilesWithPaths from '../utils/PartialTreeUtils/getCheckedFilesWithPaths.ts' export function defaultPickerIcon(): JSX.Element { return ( @@ -287,49 +288,30 @@ export default class ProviderView{ this.setLoading(true) await this.#withAbort(async (signal) => { - const newPartialTree: PartialTree = await PartialTreeUtils.afterFill( + // 1. Enrich our partialTree by fetching all 'checked' but not-yet-fetched folders + const enrichedTree: PartialTree = await PartialTreeUtils.afterFill( partialTree, (path: PartialTreeId) => this.provider.list(path, { signal }), this.validateSingleFile ) - const checkedFiles = newPartialTree.filter((item) => item.type === 'file' && item.status === 'checked') as PartialTreeFile[] - const checkedFilesWithPaths = injectPaths(newPartialTree, checkedFiles) - const uppyFiles = checkedFilesWithPaths.map((file) => file.data) - - const aggregateRestrictionError = this.plugin.uppy.validateAggregateRestrictions(uppyFiles) - + // 2. Now that we know how many files there are - recheck aggregateRestrictions! + const aggregateRestrictionError = this.validateAggregateRestrictions(enrichedTree) if (aggregateRestrictionError) { - this.plugin.setPluginState({ partialTree: newPartialTree }) + this.plugin.setPluginState({ partialTree: enrichedTree }) return } - const filesToAdd : TagFile[] = [] - const filesAlreadyAdded : TagFile[] = [] - - uppyFiles.forEach((uppyFile) => { - const tagFile = getTagFile(uppyFile, this.plugin.id, this.provider, this.plugin.opts.companionUrl) - - const id = getSafeFileId(tagFile) - if (this.plugin.uppy.checkIfFileAlreadyExists(id)) { - filesAlreadyAdded.push(tagFile) - return - } - filesToAdd.push(tagFile) - }) + // 3. Add files + const companionFiles = getCheckedFilesWithPaths(enrichedTree) + const tagFiles = companionFiles.map((f) => + getTagFile(f, this.plugin.id, this.provider, this.plugin.opts.companionUrl) + ) + addFiles(tagFiles, this.plugin.uppy) - if (filesToAdd.length > 0) { - // TODO I don't think we need to be showing this - we don't show this info when we're dropping files e.g. - this.plugin.uppy.info( - this.plugin.uppy.i18n('addedNumFiles', { numFiles: filesToAdd.length }) - ) - } - if (filesAlreadyAdded.length > 0) { - this.plugin.uppy.info(`Not adding ${filesAlreadyAdded.length} files because they already exist`) - } - this.plugin.uppy.addFiles(filesToAdd) + // 4. Reset state + this.resetPluginState() }).catch(handleError(this.plugin.uppy)) - this.setLoading(false) } @@ -369,6 +351,14 @@ export default class ProviderView{ return filtered } + validateAggregateRestrictions = (partialTree: PartialTree) => { + const checkedFiles = partialTree.filter((item) => + item.type === 'file' && item.status === 'checked' + ) as PartialTreeFile[] + const uppyFiles = checkedFiles.map((file) => file.data) + return this.plugin.uppy.validateAggregateRestrictions(uppyFiles) + } + render( state: unknown, viewOptions: RenderOpts = {} @@ -450,7 +440,7 @@ export default class ProviderView{ donePicking={this.donePicking} cancelSelection={this.cancelSelection} i18n={i18n} - validateAggregateRestrictions={this.plugin.uppy.validateAggregateRestrictions.bind(this.plugin.uppy)} + validateAggregateRestrictions={this.validateAggregateRestrictions} />
    } diff --git a/packages/@uppy/provider-views/src/SearchProviderView/SearchProviderView.tsx b/packages/@uppy/provider-views/src/SearchProviderView/SearchProviderView.tsx index 65f227ecd0..bd2e1beb5a 100644 --- a/packages/@uppy/provider-views/src/SearchProviderView/SearchProviderView.tsx +++ b/packages/@uppy/provider-views/src/SearchProviderView/SearchProviderView.tsx @@ -10,16 +10,16 @@ import Browser from '../Browser.tsx' // @ts-ignore We don't want TS to generate types for the package.json import packageJson from '../../package.json' import getTagFile from '../utils/getTagFile.ts' -import getNOfSelectedFiles from '../utils/PartialTreeUtils/getNOfSelectedFiles.ts' import PartialTreeUtils from '../utils/PartialTreeUtils' import shouldHandleScroll from '../utils/shouldHandleScroll.ts' import handleError from '../utils/handleError.ts' import getClickedRange from '../utils/getClickedRange.ts' -import injectPaths from '../utils/PartialTreeUtils/injectPaths.ts' import classNames from 'classnames' import FooterActions from '../FooterActions.tsx' import type { ValidateableFile } from '@uppy/core/lib/Restricter.ts' import remoteFileObjToLocal from '@uppy/utils/lib/remoteFileObjToLocal' +import addFiles from '../utils/addFiles.ts' +import getCheckedFilesWithPaths from '../utils/PartialTreeUtils/getCheckedFilesWithPaths.ts' const defaultState : UnknownSearchProviderPluginState = { loading: false, @@ -190,14 +190,12 @@ export default class SearchProviderView { async donePicking(): Promise { const { partialTree } = this.plugin.getPluginState() - this.plugin.uppy.log('Adding remote search provider files') - const checkedFiles = partialTree.filter((i) => i.type !== 'root' && i.status === 'checked') as PartialTreeFile[] - const checkedFilesWithPaths = injectPaths(partialTree, checkedFiles) - const uppyFiles = checkedFilesWithPaths.map((file) => file.data) - const tagFiles = uppyFiles.map((file) => - getTagFile(file, this.plugin.id, this.provider, this.plugin.opts.companionUrl) + + const companionFiles = getCheckedFilesWithPaths(partialTree) + const tagFiles = companionFiles.map((f) => + getTagFile(f, this.plugin.id, this.provider, this.plugin.opts.companionUrl) ) - this.plugin.uppy.addFiles(tagFiles) + addFiles(tagFiles, this.plugin.uppy) this.resetPluginState() } @@ -230,6 +228,14 @@ export default class SearchProviderView { } } + validateAggregateRestrictions = (partialTree: PartialTree) => { + const checkedFiles = partialTree.filter((item) => + item.type === 'file' && item.status === 'checked' + ) as PartialTreeFile[] + const uppyFiles = checkedFiles.map((file) => file.data) + return this.plugin.uppy.validateAggregateRestrictions(uppyFiles) + } + render( state: unknown, viewOptions: RenderOpts = {} @@ -293,7 +299,7 @@ export default class SearchProviderView { donePicking={this.donePicking} cancelSelection={this.cancelSelection} i18n={i18n} - validateAggregateRestrictions={this.plugin.uppy.validateAggregateRestrictions.bind(this.plugin.uppy)} + validateAggregateRestrictions={this.validateAggregateRestrictions} />
    } diff --git a/packages/@uppy/provider-views/src/utils/PartialTreeUtils/afterFill.ts b/packages/@uppy/provider-views/src/utils/PartialTreeUtils/afterFill.ts index a372e00cd1..1705fa8df4 100644 --- a/packages/@uppy/provider-views/src/utils/PartialTreeUtils/afterFill.ts +++ b/packages/@uppy/provider-views/src/utils/PartialTreeUtils/afterFill.ts @@ -1,7 +1,6 @@ import type { PartialTree, PartialTreeFile, PartialTreeFolderNode, PartialTreeId } from "@uppy/core/lib/Uppy" import type { CompanionFile } from "@uppy/utils/lib/CompanionFile" import PQueue from "p-queue" -import injectPaths from "./injectPaths" import clone from "./clone" interface ApiList { diff --git a/packages/@uppy/provider-views/src/utils/PartialTreeUtils/injectPaths.ts b/packages/@uppy/provider-views/src/utils/PartialTreeUtils/getCheckedFilesWithPaths.ts similarity index 76% rename from packages/@uppy/provider-views/src/utils/PartialTreeUtils/injectPaths.ts rename to packages/@uppy/provider-views/src/utils/PartialTreeUtils/getCheckedFilesWithPaths.ts index c1b1712ed7..00e48b2a7a 100644 --- a/packages/@uppy/provider-views/src/utils/PartialTreeUtils/injectPaths.ts +++ b/packages/@uppy/provider-views/src/utils/PartialTreeUtils/getCheckedFilesWithPaths.ts @@ -23,17 +23,20 @@ const getPath = ( } // See "Uppy file properties" documentation for `.absolutePath` and `.relativePath` (https://uppy.io/docs/uppy/#working-with-uppy-files) -const injectPaths = (partialTree: PartialTree, files: PartialTreeFile[]) : PartialTreeFile[] => { +const getCheckedFilesWithPaths = (partialTree: PartialTree) : CompanionFile[] => { const cache : Cache = {} - const injectedFiles = files.map((file) => { + // We're only interested in injecting paths into 'checked' files + const checkedFiles = partialTree.filter((item) => item.type === 'file' && item.status === 'checked') as PartialTreeFile[] + + const companionFilesWithInjectedPaths = checkedFiles.map((file) => { const path : (PartialTreeFile | PartialTreeFolderNode)[] = getPath(partialTree, file.id, cache) const absFolders = path.toReversed() const firstCheckedFolderIndex = absFolders.findIndex((i) => i.type === 'folder' && i.status === 'checked') const relFolders = absFolders.slice(firstCheckedFolderIndex) - + const absDirPath = '/' + absFolders.map((i) => i.data.name).join('/') const relDirPath = relFolders.length === 1 // Must return `undefined` (which later turns into `null` in `.getTagFile()`) @@ -42,16 +45,13 @@ const injectPaths = (partialTree: PartialTree, files: PartialTreeFile[]) : Parti : relFolders.map((i) => i.data.name).join('/') return { - ...file, - data: { - ...file.data, - absDirPath, - relDirPath - } + ...file.data, + absDirPath, + relDirPath } }) - return injectedFiles + return companionFilesWithInjectedPaths } -export default injectPaths +export default getCheckedFilesWithPaths diff --git a/packages/@uppy/provider-views/src/utils/addFiles.ts b/packages/@uppy/provider-views/src/utils/addFiles.ts new file mode 100644 index 0000000000..fc95028425 --- /dev/null +++ b/packages/@uppy/provider-views/src/utils/addFiles.ts @@ -0,0 +1,30 @@ +import type Uppy from "@uppy/core" +import type { Meta, Body, TagFile } from "@uppy/utils/lib/UppyFile" +import { getSafeFileId } from "@uppy/utils/lib/generateFileID" + +const addFiles = ( + tagFiles: TagFile[], + uppy: Uppy +) => { + const filesToAdd : TagFile[] = [] + const filesAlreadyAdded : TagFile[] = [] + tagFiles.forEach((tagFile) => { + if (uppy.checkIfFileAlreadyExists(getSafeFileId(tagFile))) { + filesAlreadyAdded.push(tagFile) + } else { + filesToAdd.push(tagFile) + } + }) + + if (filesToAdd.length > 0) { + uppy.info( + uppy.i18n('addedNumFiles', { numFiles: filesToAdd.length }) + ) + } + if (filesAlreadyAdded.length > 0) { + uppy.info(`Not adding ${filesAlreadyAdded.length} files because they already exist`) + } + uppy.addFiles(filesToAdd) +} + +export default addFiles From 6ef9ede56eaab60fd2877b5ec182eba49925c5be Mon Sep 17 00:00:00 2001 From: Evgenia Karunus Date: Fri, 17 May 2024 23:09:24 +0400 Subject: [PATCH 115/170] `getTagFile.ts` - pass fewer arguments --- .../src/ProviderView/ProviderView.tsx | 2 +- .../src/SearchProviderView/SearchProviderView.tsx | 2 +- .../@uppy/provider-views/src/utils/getTagFile.ts | 13 +++++++++---- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx b/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx index 981d7e202e..ded12df80f 100644 --- a/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx +++ b/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx @@ -305,7 +305,7 @@ export default class ProviderView{ // 3. Add files const companionFiles = getCheckedFilesWithPaths(enrichedTree) const tagFiles = companionFiles.map((f) => - getTagFile(f, this.plugin.id, this.provider, this.plugin.opts.companionUrl) + getTagFile(f, this.plugin, this.provider) ) addFiles(tagFiles, this.plugin.uppy) diff --git a/packages/@uppy/provider-views/src/SearchProviderView/SearchProviderView.tsx b/packages/@uppy/provider-views/src/SearchProviderView/SearchProviderView.tsx index bd2e1beb5a..7b303c1a6d 100644 --- a/packages/@uppy/provider-views/src/SearchProviderView/SearchProviderView.tsx +++ b/packages/@uppy/provider-views/src/SearchProviderView/SearchProviderView.tsx @@ -193,7 +193,7 @@ export default class SearchProviderView { const companionFiles = getCheckedFilesWithPaths(partialTree) const tagFiles = companionFiles.map((f) => - getTagFile(f, this.plugin.id, this.provider, this.plugin.opts.companionUrl) + getTagFile(f, this.plugin, this.provider) ) addFiles(tagFiles, this.plugin.uppy) diff --git a/packages/@uppy/provider-views/src/utils/getTagFile.ts b/packages/@uppy/provider-views/src/utils/getTagFile.ts index af37275fa0..4fe5c3ea87 100644 --- a/packages/@uppy/provider-views/src/utils/getTagFile.ts +++ b/packages/@uppy/provider-views/src/utils/getTagFile.ts @@ -1,16 +1,21 @@ +import type { UnknownPlugin } from "@uppy/core" import type { CompanionClientProvider, CompanionClientSearchProvider } from "@uppy/utils/lib/CompanionClientProvider" import type { CompanionFile } from "@uppy/utils/lib/CompanionFile" -import type { Meta, TagFile } from "@uppy/utils/lib/UppyFile" +import type { Meta, Body, TagFile } from "@uppy/utils/lib/UppyFile" import getFileType from "@uppy/utils/lib/getFileType" import isPreviewSupported from "@uppy/utils/lib/isPreviewSupported" // TODO: document what is a "tagFile" or get rid of this concept -const getTagFile = (file: CompanionFile, pluginId: string, provider: CompanionClientProvider | CompanionClientSearchProvider, companionUrl: string) : TagFile => { +const getTagFile = ( + file: CompanionFile, + plugin: UnknownPlugin, + provider: CompanionClientProvider | CompanionClientSearchProvider, +) : TagFile => { const fileType = getFileType({ type: file.mimeType, name: file.name }) const tagFile: TagFile = { id: file.id, - source: pluginId, + source: plugin.id, name: file.name || file.id, type: file.mimeType, isRemote: true, @@ -30,7 +35,7 @@ const getTagFile = (file: CompanionFile, pluginId: string, provi fileId: file.id, }, remote: { - companionUrl, + companionUrl: plugin.opts.companionUrl, // @ts-expect-error untyped for now url: `${provider.fileUrl(file.requestPath)}`, body: { From b814da3bbe76a11f179a50b7d024f55d25c13b41 Mon Sep 17 00:00:00 2001 From: Evgenia Karunus Date: Fri, 17 May 2024 23:24:44 +0400 Subject: [PATCH 116/170] `addFiles.ts` - move conversion to tagFiles into `addFiles()` --- .../src/ProviderView/ProviderView.tsx | 5 +--- .../SearchProviderView/SearchProviderView.tsx | 7 +++--- .../provider-views/src/utils/addFiles.ts | 24 ++++++++++++------- .../provider-views/src/utils/getTagFile.ts | 2 +- 4 files changed, 21 insertions(+), 17 deletions(-) diff --git a/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx b/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx index ded12df80f..e4d73bbc25 100644 --- a/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx +++ b/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx @@ -304,10 +304,7 @@ export default class ProviderView{ // 3. Add files const companionFiles = getCheckedFilesWithPaths(enrichedTree) - const tagFiles = companionFiles.map((f) => - getTagFile(f, this.plugin, this.provider) - ) - addFiles(tagFiles, this.plugin.uppy) + addFiles(companionFiles, this.plugin, this.provider) // 4. Reset state this.resetPluginState() diff --git a/packages/@uppy/provider-views/src/SearchProviderView/SearchProviderView.tsx b/packages/@uppy/provider-views/src/SearchProviderView/SearchProviderView.tsx index 7b303c1a6d..2d0f9723c2 100644 --- a/packages/@uppy/provider-views/src/SearchProviderView/SearchProviderView.tsx +++ b/packages/@uppy/provider-views/src/SearchProviderView/SearchProviderView.tsx @@ -191,12 +191,11 @@ export default class SearchProviderView { async donePicking(): Promise { const { partialTree } = this.plugin.getPluginState() + // 1. Add files const companionFiles = getCheckedFilesWithPaths(partialTree) - const tagFiles = companionFiles.map((f) => - getTagFile(f, this.plugin, this.provider) - ) - addFiles(tagFiles, this.plugin.uppy) + addFiles(companionFiles, this.plugin, this.provider) + // 2. Reset state this.resetPluginState() } diff --git a/packages/@uppy/provider-views/src/utils/addFiles.ts b/packages/@uppy/provider-views/src/utils/addFiles.ts index fc95028425..520d7993d9 100644 --- a/packages/@uppy/provider-views/src/utils/addFiles.ts +++ b/packages/@uppy/provider-views/src/utils/addFiles.ts @@ -1,15 +1,23 @@ -import type Uppy from "@uppy/core" +import type { UnknownPlugin } from "@uppy/core" +import type { CompanionClientProvider, CompanionClientSearchProvider } from "@uppy/utils/lib/CompanionClientProvider" +import type { CompanionFile } from "@uppy/utils/lib/CompanionFile" import type { Meta, Body, TagFile } from "@uppy/utils/lib/UppyFile" import { getSafeFileId } from "@uppy/utils/lib/generateFileID" +import getTagFile from "./getTagFile" const addFiles = ( - tagFiles: TagFile[], - uppy: Uppy + companionFiles: CompanionFile[], + plugin: UnknownPlugin, + provider: CompanionClientProvider | CompanionClientSearchProvider, ) => { + const tagFiles: TagFile[] = companionFiles.map((f) => + getTagFile(f, plugin, provider) + ) + const filesToAdd : TagFile[] = [] const filesAlreadyAdded : TagFile[] = [] tagFiles.forEach((tagFile) => { - if (uppy.checkIfFileAlreadyExists(getSafeFileId(tagFile))) { + if (plugin.uppy.checkIfFileAlreadyExists(getSafeFileId(tagFile))) { filesAlreadyAdded.push(tagFile) } else { filesToAdd.push(tagFile) @@ -17,14 +25,14 @@ const addFiles = ( }) if (filesToAdd.length > 0) { - uppy.info( - uppy.i18n('addedNumFiles', { numFiles: filesToAdd.length }) + plugin.uppy.info( + plugin.uppy.i18n('addedNumFiles', { numFiles: filesToAdd.length }) ) } if (filesAlreadyAdded.length > 0) { - uppy.info(`Not adding ${filesAlreadyAdded.length} files because they already exist`) + plugin.uppy.info(`Not adding ${filesAlreadyAdded.length} files because they already exist`) } - uppy.addFiles(filesToAdd) + plugin.uppy.addFiles(filesToAdd) } export default addFiles diff --git a/packages/@uppy/provider-views/src/utils/getTagFile.ts b/packages/@uppy/provider-views/src/utils/getTagFile.ts index 4fe5c3ea87..86d016e590 100644 --- a/packages/@uppy/provider-views/src/utils/getTagFile.ts +++ b/packages/@uppy/provider-views/src/utils/getTagFile.ts @@ -12,7 +12,7 @@ const getTagFile = ( provider: CompanionClientProvider | CompanionClientSearchProvider, ) : TagFile => { const fileType = getFileType({ type: file.mimeType, name: file.name }) - + const tagFile: TagFile = { id: file.id, source: plugin.id, From 9a506f5188f361d2fbb4547112dc3666f639b785 Mon Sep 17 00:00:00 2001 From: Evgenia Karunus Date: Mon, 20 May 2024 19:07:05 +0400 Subject: [PATCH 117/170] `uppy.validateRestrictions()` - remove legacy method --- packages/@uppy/core/src/Uppy.test.ts | 12 +++++------- packages/@uppy/core/src/Uppy.ts | 12 ------------ 2 files changed, 5 insertions(+), 19 deletions(-) diff --git a/packages/@uppy/core/src/Uppy.test.ts b/packages/@uppy/core/src/Uppy.test.ts index 6f73e34452..5dac151243 100644 --- a/packages/@uppy/core/src/Uppy.test.ts +++ b/packages/@uppy/core/src/Uppy.test.ts @@ -2150,7 +2150,7 @@ describe('src/Core', () => { ) }) - it('should check if a file validateRestrictions', () => { + it('should report error on validateSingleFile', () => { const core = new Core({ restrictions: { minFileSize: 300000, @@ -2175,15 +2175,13 @@ describe('src/Core', () => { size: 270733, } - // @ts-ignore - const validateRestrictions1 = core.validateRestrictions(newFile) - // @ts-ignore - const validateRestrictions2 = core2.validateRestrictions(newFile) + const validateRestrictions1 = core.validateSingleFile(newFile) + const validateRestrictions2 = core2.validateSingleFile(newFile) - expect(validateRestrictions1!.message).toEqual( + expect(validateRestrictions1).toEqual( 'This file is smaller than the allowed size of 293 KB', ) - expect(validateRestrictions2!.message).toEqual( + expect(validateRestrictions2).toEqual( 'You can only upload: image/png', ) }) diff --git a/packages/@uppy/core/src/Uppy.ts b/packages/@uppy/core/src/Uppy.ts index abd1f2b9c8..20fde8d3eb 100644 --- a/packages/@uppy/core/src/Uppy.ts +++ b/packages/@uppy/core/src/Uppy.ts @@ -865,18 +865,6 @@ export class Uppy { } } - validateRestrictions( - file: ValidateableFile, - files: ValidateableFile[] = this.getFiles(), - ): RestrictionError | null { - try { - this.#restricter.validate(files, [file]) - } catch (err) { - return err - } - return null - } - validateSingleFile(file: ValidateableFile): string | null { try { this.#restricter.validateSingleFile(file) From 1492e33401efbe5c4dbb941252d242d682a096d0 Mon Sep 17 00:00:00 2001 From: Evgenia Karunus Date: Mon, 20 May 2024 20:12:20 +0400 Subject: [PATCH 118/170] `uppy.validateAggregateRestrictions()` - make aggregate restricter report aggregate error --- packages/@uppy/core/src/Restricter.ts | 26 +++++++++----------------- packages/@uppy/core/src/locale.ts | 1 + 2 files changed, 10 insertions(+), 17 deletions(-) diff --git a/packages/@uppy/core/src/Restricter.ts b/packages/@uppy/core/src/Restricter.ts index 4c692301d6..966250bd3d 100644 --- a/packages/@uppy/core/src/Restricter.ts +++ b/packages/@uppy/core/src/Restricter.ts @@ -96,25 +96,17 @@ class Restricter { } if (maxTotalFileSize) { - let totalFilesSize = existingFiles.reduce( - (total, f) => (total + (f.size ?? 0)) as number, + let totalFilesSize = [...existingFiles, ...addingFiles].reduce( + (total, f) => total + (f.size ?? 0), 0, ) - - for (const addingFile of addingFiles) { - if (addingFile.size != null) { - // We can't check maxTotalFileSize if the size is unknown. - totalFilesSize += addingFile.size - - if (totalFilesSize > maxTotalFileSize) { - throw new RestrictionError( - this.i18n('exceedsSize', { - size: prettierBytes(maxTotalFileSize), - file: addingFile.name, - }), - ) - } - } + if (totalFilesSize > maxTotalFileSize) { + throw new RestrictionError( + this.i18n('aggregateExceedsSize', { + sizeAllowed: prettierBytes(maxTotalFileSize), + size: prettierBytes(totalFilesSize), + }), + ) } } } diff --git a/packages/@uppy/core/src/locale.ts b/packages/@uppy/core/src/locale.ts index 92674805d6..85f5164284 100644 --- a/packages/@uppy/core/src/locale.ts +++ b/packages/@uppy/core/src/locale.ts @@ -12,6 +12,7 @@ export default { 0: 'You have to select at least %{smart_count} file', 1: 'You have to select at least %{smart_count} files', }, + aggregateExceedsSize: 'You selected %{size} of files, but maximum allowed size is %{sizeAllowed}', exceedsSize: '%{file} exceeds maximum allowed size of %{size}', missingRequiredMetaField: 'Missing required meta fields', missingRequiredMetaFieldOnFile: From f58d6e5650d6410f17427c3dd9aeb3562eb8fc26 Mon Sep 17 00:00:00 2001 From: Evgenia Karunus Date: Mon, 20 May 2024 20:47:52 +0400 Subject: [PATCH 119/170] css - make aggregate errors look nice --- .../provider-views/src/FooterActions.tsx | 44 ++++++++++--------- packages/@uppy/provider-views/src/style.scss | 23 +++++++++- 2 files changed, 44 insertions(+), 23 deletions(-) diff --git a/packages/@uppy/provider-views/src/FooterActions.tsx b/packages/@uppy/provider-views/src/FooterActions.tsx index 059e77d654..c148e5ec0e 100644 --- a/packages/@uppy/provider-views/src/FooterActions.tsx +++ b/packages/@uppy/provider-views/src/FooterActions.tsx @@ -34,30 +34,32 @@ export default function FooterActions({ return (
    - - +
    + + +
    { aggregateRestrictionError && -
    +
    {aggregateRestrictionError}
    } diff --git a/packages/@uppy/provider-views/src/style.scss b/packages/@uppy/provider-views/src/style.scss index 1a7ce23176..766dddf22a 100644 --- a/packages/@uppy/provider-views/src/style.scss +++ b/packages/@uppy/provider-views/src/style.scss @@ -346,8 +346,8 @@ .uppy-ProviderBrowser-footer { display: flex; align-items: center; - height: 65px; - padding: 0 15px; + justify-content: space-between; + padding: 15px; background-color: $white; border-top: 1px solid $gray-200; @@ -360,3 +360,22 @@ border-top: 1px solid $gray-800; } } + +.uppy-ProviderBrowser-footer-buttons { + flex-shrink: 0; +} + +.uppy-ProviderBrowser-footer-error { + color: $red; + line-height: 18px; +} + +@media (max-width: 426px) { + .uppy-ProviderBrowser-footer { + flex-direction: column-reverse; + align-items: stretch; + } + .uppy-ProviderBrowser-footer-error{ + padding-bottom: 10px; + } +} From eb1951ba83c42689f971cf4bd1776eb63b6795f5 Mon Sep 17 00:00:00 2001 From: Evgenia Karunus Date: Mon, 20 May 2024 21:54:16 +0400 Subject: [PATCH 120/170] `PartialTreeUtils/index.test.ts` - accommodate tests to the latest changes --- .../src/utils/PartialTreeUtils/index.test.ts | 52 ++++++++++--------- 1 file changed, 28 insertions(+), 24 deletions(-) diff --git a/packages/@uppy/provider-views/src/utils/PartialTreeUtils/index.test.ts b/packages/@uppy/provider-views/src/utils/PartialTreeUtils/index.test.ts index 69f6c74b2f..9f246f1fa9 100644 --- a/packages/@uppy/provider-views/src/utils/PartialTreeUtils/index.test.ts +++ b/packages/@uppy/provider-views/src/utils/PartialTreeUtils/index.test.ts @@ -5,7 +5,7 @@ import type { CompanionFile } from '@uppy/utils/lib/CompanionFile' import afterOpenFolder from './afterOpenFolder.ts' import afterScrollFolder from './afterScrollFolder.ts' import afterFill from './afterFill.ts' -import injectPaths from './injectPaths.ts' +import getCheckedFilesWithPaths from './getCheckedFilesWithPaths.ts' import getNOfSelectedFiles from './getNOfSelectedFiles.ts' const _root = (id: string, options: any = {}) : PartialTreeFolderRoot => ({ @@ -55,11 +55,11 @@ const getFile = (tree: PartialTree, id: string) => tree.find((i) => i.id === id) as PartialTreeFile describe('afterFill()', () => { - it('fetches an already loaded file', async () => { + it('preserves .checked files in an already .cached folder', async () => { const tree : PartialTree = [ _root('ourRoot'), _folder('1', { parentId: 'ourRoot' }), - _folder('2', { parentId: 'ourRoot' }), + _folder('2', { parentId: 'ourRoot', cached: true }), _file('2_1', { parentId: '2' }), _file('2_2', { parentId: '2', status: 'checked' }), _file('2_3', { parentId: '2' }), @@ -68,13 +68,14 @@ describe('afterFill()', () => { _file('4', { parentId: 'ourRoot' }), ] const mock = vi.fn() - const result = await afterFill(tree, mock) + const enrichedTree = await afterFill(tree, mock, () => null) // While we're at it - make sure we're not doing excessive api calls! expect(mock.mock.calls.length).toEqual(0) - expect(result.length).toEqual(1) - expect(result[0].id).toEqual('2_2') + const checkedFiles = enrichedTree.filter((item) => item.type === 'file' && item.status === 'checked') + expect(checkedFiles.length).toEqual(1) + expect(checkedFiles[0].id).toEqual('2_2') }) it('fetches a .checked folder', async () => { @@ -93,10 +94,11 @@ describe('afterFill()', () => { } return Promise.reject() } - const result = await afterFill(tree, mock) + const enrichedTree = await afterFill(tree, mock, () => null) - expect(result.length).toEqual(4) - expect(result.map((f) => f.id)).toEqual(['2_1', '2_2', '2_3', '2_4']) + const checkedFiles = enrichedTree.filter((item) => item.type === 'file' && item.status === 'checked') + expect(checkedFiles.length).toEqual(4) + expect(checkedFiles.map((f) => f.id)).toEqual(['2_1', '2_2', '2_3', '2_4']) }) it('fetches remaining pages in a folder', async () => { @@ -112,10 +114,11 @@ describe('afterFill()', () => { } return Promise.reject() } - const result = await afterFill(tree, mock) + const enrichedTree = await afterFill(tree, mock, () => null) - expect(result.length).toEqual(2) - expect(result.map((f) => f.id)).toEqual(['111', '222']) + const checkedFiles = enrichedTree.filter((item) => item.type === 'file' && item.status === 'checked') + expect(checkedFiles.length).toEqual(2) + expect(checkedFiles.map((f) => f.id)).toEqual(['111', '222']) }) it('fetches a folder two levels deep', async () => { @@ -136,10 +139,11 @@ describe('afterFill()', () => { } return Promise.reject() } - const result = await afterFill(tree, mock) + const enrichedTree = await afterFill(tree, mock, () => null) - expect(result.length).toEqual(5) - expect(result.map((f) => f.id)).toEqual(['2_1', '2_2', '2_3', '666_1', '666_2']) + const checkedFiles = enrichedTree.filter((item) => item.type === 'file' && item.status === 'checked') + expect(checkedFiles.length).toEqual(5) + expect(checkedFiles.map((f) => f.id)).toEqual(['2_1', '2_2', '2_3', '666_1', '666_2']) }) it('complex situation', async () => { @@ -178,10 +182,11 @@ describe('afterFill()', () => { } return Promise.reject() } - const result = await afterFill(tree, mock) + const enrichedTree = await afterFill(tree, mock, () => null) - expect(result.length).toEqual(8) - expect(result.map((f) => f.id)).toEqual(['2_1', '2_2', '3_1', '2_3', '666_1', '777_1', '777_2_1', '777_2_1_1']) + const checkedFiles = enrichedTree.filter((item) => item.type === 'file' && item.status === 'checked') + expect(checkedFiles.length).toEqual(8) + expect(checkedFiles.map((f) => f.id)).toEqual(['2_1', '2_2', '3_1', '2_3', '666_1', '777_1', '777_2_1', '777_2_1_1']) }) }) @@ -197,7 +202,7 @@ describe('afterOpenFolder()', () => { const clickedFolder = oldPartialTree.find((f) => f.id === '2') as PartialTreeFolderNode - const newTree = afterOpenFolder(oldPartialTree, fakeCompanionFiles, clickedFolder, () => null, null) + const newTree = afterOpenFolder(oldPartialTree, fakeCompanionFiles, clickedFolder, null, () => null) expect(getFolder(newTree, '666').status).toEqual('checked') expect(getFile(newTree, '777').status).toEqual('checked') @@ -215,7 +220,7 @@ describe('afterOpenFolder()', () => { const clickedFolder = oldPartialTree.find((f) => f.id === '2') as PartialTreeFolderNode - const newTree = afterOpenFolder(oldPartialTree, fakeCompanionFiles, clickedFolder, () => null, null) + const newTree = afterOpenFolder(oldPartialTree, fakeCompanionFiles, clickedFolder, null, () => null) expect(getFolder(newTree, '666').status).toEqual('unchecked') expect(getFile(newTree, '777').status).toEqual('unchecked') @@ -401,25 +406,24 @@ describe('injectPaths()', () => { _file('3', { parentId: 'ourRoot' }), _file('4', { parentId: 'ourRoot' }), ] - const checkedFiles = tree.filter((item) => item.type === 'file' && item.status === 'checked') as PartialTreeFile[] // These test cases are based on documentation for .absolutePath and .relativePath (https://uppy.io/docs/uppy/#filemeta) it('.absolutePath always begins with / + always ends with the file’s name.', () => { - const result = injectPaths(tree, checkedFiles) + const result = getCheckedFilesWithPaths(tree) expect(result.find((f) => f.id === '2_2')!.absDirPath).toEqual('/name_2/name_2_2.jpg') expect(result.find((f) => f.id === '2_4_3')!.absDirPath).toEqual('/name_2/name_2_4/name_2_4_3.jpg') }) it('.relativePath is null when file is selected independently', () => { - const result = injectPaths(tree, checkedFiles) + const result = getCheckedFilesWithPaths(tree) // .relDirPath should be `undefined`, which will make .relativePath `null` eventually expect(result.find((f) => f.id === '2_2')!.relDirPath).toEqual(undefined) }) it('.relativePath attends to highest checked folder', () => { - const result = injectPaths(tree, checkedFiles) + const result = getCheckedFilesWithPaths(tree) expect(result.find((f) => f.id === '2_4_1')!.relDirPath).toEqual('name_2_4/name_2_4_1.jpg') }) From 5dad68d4acb1612f74771a01c60b9052a10db028 Mon Sep 17 00:00:00 2001 From: Evgenia Karunus Date: Mon, 20 May 2024 22:08:23 +0400 Subject: [PATCH 121/170] tests - make all uppy tests work --- packages/@uppy/core/src/Uppy.test.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/@uppy/core/src/Uppy.test.ts b/packages/@uppy/core/src/Uppy.test.ts index 5dac151243..49e1934563 100644 --- a/packages/@uppy/core/src/Uppy.test.ts +++ b/packages/@uppy/core/src/Uppy.test.ts @@ -2127,7 +2127,7 @@ describe('src/Core', () => { it('should enforce the maxTotalFileSize rule', () => { const core = new Core({ restrictions: { - maxTotalFileSize: 34000, + maxTotalFileSize: 20000, }, }) @@ -2146,7 +2146,9 @@ describe('src/Core', () => { data: testImage, }) }).toThrowError( - new Error('foo1.jpg exceeds maximum allowed size of 33 KB'), + new Error( + 'You selected 34 KB of files, but maximum allowed size is 20 KB', + ), ) }) @@ -2181,9 +2183,7 @@ describe('src/Core', () => { expect(validateRestrictions1).toEqual( 'This file is smaller than the allowed size of 293 KB', ) - expect(validateRestrictions2).toEqual( - 'You can only upload: image/png', - ) + expect(validateRestrictions2).toEqual('You can only upload: image/png') }) it('should emit `restriction-failed` event when some rule is violated', () => { From 090376af514dc4763e3b90a3a2c7b857e50a8e59 Mon Sep 17 00:00:00 2001 From: Evgenia Karunus Date: Fri, 31 May 2024 10:35:51 +0400 Subject: [PATCH 122/170] prettiness - run `yarn format` --- packages/@uppy/core/src/Uppy.ts | 9 +- packages/@uppy/core/src/_common.scss | 4 +- packages/@uppy/core/src/locale.ts | 3 +- packages/@uppy/facebook/src/Facebook.tsx | 2 +- .../@uppy/provider-views/src/Breadcrumbs.tsx | 2 +- packages/@uppy/provider-views/src/Browser.tsx | 13 +- .../provider-views/src/FooterActions.tsx | 19 +- .../src/Item/components/GridItem.tsx | 2 +- .../src/Item/components/ListItem.tsx | 9 +- .../@uppy/provider-views/src/Item/index.tsx | 33 +- .../src/ProviderView/Header.tsx | 6 +- .../src/ProviderView/ProviderView.tsx | 275 ++++++++----- .../provider-views/src/ProviderView/User.tsx | 5 +- .../provider-views/src/SearchFilterInput.tsx | 4 +- .../SearchProviderView/SearchProviderView.tsx | 219 ++++++----- packages/@uppy/provider-views/src/style.scss | 2 +- .../uppy-ProviderBrowser-viewType--list.scss | 5 +- .../src/utils/PartialTreeUtils/afterFill.ts | 51 ++- .../utils/PartialTreeUtils/afterOpenFolder.ts | 31 +- .../PartialTreeUtils/afterScrollFolder.ts | 33 +- .../PartialTreeUtils/afterToggleCheckbox.ts | 68 +++- .../src/utils/PartialTreeUtils/clone.ts | 2 +- .../getCheckedFilesWithPaths.ts | 44 ++- .../PartialTreeUtils/getNOfSelectedFiles.ts | 8 +- .../src/utils/PartialTreeUtils/index.test.ts | 370 +++++++++++------- .../src/utils/PartialTreeUtils/index.ts | 2 +- .../provider-views/src/utils/addFiles.ts | 27 +- .../src/utils/getClickedRange.ts | 26 +- .../provider-views/src/utils/getTagFile.ts | 19 +- .../provider-views/src/utils/handleError.ts | 42 +- .../src/utils/shouldHandleScroll.ts | 5 +- .../utils/src/CompanionClientProvider.ts | 2 +- yarn.lock | 6 +- 33 files changed, 840 insertions(+), 508 deletions(-) diff --git a/packages/@uppy/core/src/Uppy.ts b/packages/@uppy/core/src/Uppy.ts index 8286ac70ea..967ae11589 100644 --- a/packages/@uppy/core/src/Uppy.ts +++ b/packages/@uppy/core/src/Uppy.ts @@ -161,10 +161,7 @@ export type UnknownSearchProviderPluginState = { isInputMode: boolean } & Pick< UnknownProviderPluginState, - | 'loading' - | 'searchString' - | 'partialTree' - | 'currentFolderId' + 'loading' | 'searchString' | 'partialTree' | 'currentFolderId' > export type UnknownSearchProviderPlugin< M extends Meta, @@ -854,7 +851,9 @@ export class Uppy { return null } - validateAggregateRestrictions(files: ValidateableFile[]): string | null { + validateAggregateRestrictions( + files: ValidateableFile[], + ): string | null { const existingFiles = this.getFiles() try { this.#restricter.validateAggregateRestrictions(existingFiles, files) diff --git a/packages/@uppy/core/src/_common.scss b/packages/@uppy/core/src/_common.scss index a6179feaa4..ac88cdc196 100644 --- a/packages/@uppy/core/src/_common.scss +++ b/packages/@uppy/core/src/_common.scss @@ -146,8 +146,8 @@ @include blue-border-focus--dark; } - &.uppy-c-btn--disabled{ - background-color: rgb(142, 178, 219) + &.uppy-c-btn--disabled { + background-color: rgb(142, 178, 219); } } diff --git a/packages/@uppy/core/src/locale.ts b/packages/@uppy/core/src/locale.ts index 85f5164284..90a9974499 100644 --- a/packages/@uppy/core/src/locale.ts +++ b/packages/@uppy/core/src/locale.ts @@ -12,7 +12,8 @@ export default { 0: 'You have to select at least %{smart_count} file', 1: 'You have to select at least %{smart_count} files', }, - aggregateExceedsSize: 'You selected %{size} of files, but maximum allowed size is %{sizeAllowed}', + aggregateExceedsSize: + 'You selected %{size} of files, but maximum allowed size is %{sizeAllowed}', exceedsSize: '%{file} exceeds maximum allowed size of %{size}', missingRequiredMetaField: 'Missing required meta fields', missingRequiredMetaFieldOnFile: diff --git a/packages/@uppy/facebook/src/Facebook.tsx b/packages/@uppy/facebook/src/Facebook.tsx index 5c255241ea..fc90357c21 100644 --- a/packages/@uppy/facebook/src/Facebook.tsx +++ b/packages/@uppy/facebook/src/Facebook.tsx @@ -110,7 +110,7 @@ export default class Facebook extends UIPlugin< return this.view.render(state, { viewType: 'grid', showFilter: false, - showTitles: false + showTitles: false, }) } else { return this.view.render(state) diff --git a/packages/@uppy/provider-views/src/Breadcrumbs.tsx b/packages/@uppy/provider-views/src/Breadcrumbs.tsx index b04bbc4265..ba008b3202 100644 --- a/packages/@uppy/provider-views/src/Breadcrumbs.tsx +++ b/packages/@uppy/provider-views/src/Breadcrumbs.tsx @@ -25,7 +25,7 @@ export default function Breadcrumbs( type="button" className="uppy-u-reset uppy-c-btn" onClick={() => openFolder(folder.id)} - > + > {folder.type === 'root' ? title : folder.data.name} {breadcrumbs.length === index + 1 ? '' : ' / '} diff --git a/packages/@uppy/provider-views/src/Browser.tsx b/packages/@uppy/provider-views/src/Browser.tsx index c75da89385..a5cf76f34a 100644 --- a/packages/@uppy/provider-views/src/Browser.tsx +++ b/packages/@uppy/provider-views/src/Browser.tsx @@ -7,14 +7,17 @@ import VirtualList from '@uppy/utils/lib/VirtualList' import type { Body, Meta } from '@uppy/utils/lib/UppyFile' import type { I18n } from '@uppy/utils/lib/Translator' import Item from './Item/index.tsx' -import type { PartialTreeFile, PartialTreeFolderNode } from '@uppy/core/lib/Uppy.ts' +import type { + PartialTreeFile, + PartialTreeFolderNode, +} from '@uppy/core/lib/Uppy.ts' import type { RestrictionError } from '@uppy/core/lib/Restricter.ts' import type { CompanionFile } from '@uppy/utils/lib/CompanionFile' import { useEffect, useState } from 'preact/hooks' import type ProviderView from './ProviderView/ProviderView.tsx' type BrowserProps = { - displayedPartialTree: (PartialTreeFile | PartialTreeFolderNode)[], + displayedPartialTree: (PartialTreeFile | PartialTreeFolderNode)[] viewType: string toggleCheckbox: ProviderView['toggleCheckbox'] handleScroll: ProviderView['handleScroll'] @@ -68,11 +71,7 @@ function Browser(props: BrowserProps) { } if (displayedPartialTree.length === 0) { - return ( -
    - {noResultsLabel} -
    - ) + return
    {noResultsLabel}
    } const renderItem = (item: PartialTreeFile | PartialTreeFolderNode) => ( diff --git a/packages/@uppy/provider-views/src/FooterActions.tsx b/packages/@uppy/provider-views/src/FooterActions.tsx index c148e5ec0e..a86660c127 100644 --- a/packages/@uppy/provider-views/src/FooterActions.tsx +++ b/packages/@uppy/provider-views/src/FooterActions.tsx @@ -12,13 +12,16 @@ export default function FooterActions({ donePicking, i18n, partialTree, - validateAggregateRestrictions + validateAggregateRestrictions, }: { cancelSelection: ProviderView['cancelSelection'] donePicking: ProviderView['donePicking'] i18n: I18n partialTree: PartialTree - validateAggregateRestrictions: ProviderView['validateAggregateRestrictions'] + validateAggregateRestrictions: ProviderView< + M, + B + >['validateAggregateRestrictions'] }) { const aggregateRestrictionError = useMemo(() => { return validateAggregateRestrictions(partialTree) @@ -36,10 +39,9 @@ export default function FooterActions({
    - { - aggregateRestrictionError && + {aggregateRestrictionError && (
    {aggregateRestrictionError}
    - } + )}
    ) } diff --git a/packages/@uppy/provider-views/src/Item/components/GridItem.tsx b/packages/@uppy/provider-views/src/Item/components/GridItem.tsx index 60386dff36..6043ccb0b5 100644 --- a/packages/@uppy/provider-views/src/Item/components/GridItem.tsx +++ b/packages/@uppy/provider-views/src/Item/components/GridItem.tsx @@ -43,7 +43,7 @@ function GridItem( onChange={toggleCheckbox} name="listitem" id={id} - checked={status === "checked" ? true : false} + checked={status === 'checked' ? true : false} disabled={isDisabled} data-uppy-super-focusable /> diff --git a/packages/@uppy/provider-views/src/Item/components/ListItem.tsx b/packages/@uppy/provider-views/src/Item/components/ListItem.tsx index e0ac6a5e90..29e21df764 100644 --- a/packages/@uppy/provider-views/src/Item/components/ListItem.tsx +++ b/packages/@uppy/provider-views/src/Item/components/ListItem.tsx @@ -55,8 +55,12 @@ export default function ListItem( // for the
    {showTitles && {title}} + }
  • ) diff --git a/packages/@uppy/provider-views/src/Item/index.tsx b/packages/@uppy/provider-views/src/Item/index.tsx index 94a7334708..1e67d587b4 100644 --- a/packages/@uppy/provider-views/src/Item/index.tsx +++ b/packages/@uppy/provider-views/src/Item/index.tsx @@ -7,7 +7,11 @@ import type { Meta, Body } from '@uppy/utils/lib/UppyFile' import ItemIcon from './components/ItemIcon.tsx' import GridItem from './components/GridItem.tsx' import ListItem from './components/ListItem.tsx' -import type { PartialTreeFile, PartialTreeFolderNode, PartialTreeId } from '@uppy/core/lib/Uppy.ts' +import type { + PartialTreeFile, + PartialTreeFolderNode, + PartialTreeId, +} from '@uppy/core/lib/Uppy.ts' type ItemProps = { viewType: string @@ -40,23 +44,24 @@ export default function Item( { 'uppy-ProviderBrowserItem--disabled': isDisabled }, { 'uppy-ProviderBrowserItem--noPreview': file.data.icon === 'video' }, { 'uppy-ProviderBrowserItem--is-checked': file.status === 'checked' }, - { 'uppy-ProviderBrowserItem--is-partial': file.status === 'partial' } + { 'uppy-ProviderBrowserItem--is-partial': file.status === 'partial' }, ), itemIconEl: , isDisabled, - restrictionError + restrictionError, } - let ourProps = file.data.isFolder ? - { - ...sharedProps, - type: 'folder', - handleFolderClick: () => openFolder(file.id), - } : - { - ...sharedProps, - type: 'file' - } + let ourProps = + file.data.isFolder ? + { + ...sharedProps, + type: 'folder', + handleFolderClick: () => openFolder(file.id), + } + : { + ...sharedProps, + type: 'file', + } switch (viewType) { case 'grid': @@ -65,7 +70,7 @@ export default function Item( return {...ourProps} /> case 'unsplash': return ( - {...ourProps} > + {...ourProps}> ( title={props.title} /> )} - + ) diff --git a/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx b/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx index 04ecaa8143..352d8ca86c 100644 --- a/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx +++ b/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx @@ -48,21 +48,23 @@ export function defaultPickerIcon(): JSX.Element { ) } -const getDefaultState = (rootFolderId: string | null) : UnknownProviderPluginState => ({ +const getDefaultState = ( + rootFolderId: string | null, +): UnknownProviderPluginState => ({ authenticated: undefined, // we don't know yet partialTree: [ { type: 'root', id: rootFolderId, cached: false, - nextPagePath: null - } + nextPagePath: null, + }, ], currentFolderId: rootFolderId, searchString: '', didFirstRender: false, username: null, - loading: false + loading: false, }) type Optional = Pick, K> & Omit @@ -81,14 +83,20 @@ interface Opts { onAuth: (authFormData: unknown) => Promise }) => h.JSX.Element } -type PassedOpts = Optional, 'viewType' | 'showTitles' | 'showFilter' | 'showBreadcrumbs' | 'loadAllFiles'> +type PassedOpts = Optional< + Opts, + 'viewType' | 'showTitles' | 'showFilter' | 'showBreadcrumbs' | 'loadAllFiles' +> type DefaultOpts = Omit, 'provider'> -type RenderOpts = Omit, 'provider'> +type RenderOpts = Omit< + PassedOpts, + 'provider' +> /** * Class to easily generate generic views for Provider plugins */ -export default class ProviderView{ +export default class ProviderView { static VERSION = packageJson.version plugin: UnknownProviderPlugin @@ -98,14 +106,11 @@ export default class ProviderView{ isHandlingScroll: boolean = false lastCheckbox: string | null = null - constructor( - plugin: UnknownProviderPlugin, - opts: PassedOpts, - ) { + constructor(plugin: UnknownProviderPlugin, opts: PassedOpts) { this.plugin = plugin this.provider = opts.provider - const defaultOptions : DefaultOpts = { + const defaultOptions: DefaultOpts = { viewType: 'list', showTitles: true, showFilter: true, @@ -130,7 +135,10 @@ export default class ProviderView{ // @ts-expect-error this should be typed in @uppy/dashboard. this.plugin.uppy.on('dashboard:close-panel', this.resetPluginState) - this.plugin.uppy.registerRequestClient(this.provider.provider, this.provider) + this.plugin.uppy.registerRequestClient( + this.provider.provider, + this.provider, + ) } resetPluginState(): void { @@ -147,8 +155,8 @@ export default class ProviderView{ cancelSelection(): void { const { partialTree } = this.plugin.getPluginState() - const newPartialTree : PartialTree = partialTree.map((item) => - item.type === 'root' ? item : { ...item, status: 'unchecked' } + const newPartialTree: PartialTree = partialTree.map((item) => + item.type === 'root' ? item : { ...item, status: 'unchecked' }, ) this.plugin.setPluginState({ partialTree: newPartialTree }) } @@ -183,13 +191,22 @@ export default class ProviderView{ async openFolder(folderId: string | null): Promise { this.lastCheckbox = null - console.log(`____________________________________________GETTING FOLDER "${folderId}"`); + console.log( + `____________________________________________GETTING FOLDER "${folderId}"`, + ) // Returning cached folder const { partialTree } = this.plugin.getPluginState() - const clickedFolder = partialTree.find((folder) => folder.id === folderId)! as PartialTreeFolder + const clickedFolder = partialTree.find( + (folder) => folder.id === folderId, + )! as PartialTreeFolder if (clickedFolder.cached) { - console.log("Folder was cached____________________________________________"); - this.plugin.setPluginState({ currentFolderId: folderId, searchString: '' }) + console.log( + 'Folder was cached____________________________________________', + ) + this.plugin.setPluginState({ + currentFolderId: folderId, + searchString: '', + }) return } @@ -198,21 +215,32 @@ export default class ProviderView{ let currentPagePath = folderId let currentItems: CompanionFile[] = [] do { - const { username, nextPagePath, items } = await this.provider.list(currentPagePath, { signal }) + const { username, nextPagePath, items } = await this.provider.list( + currentPagePath, + { signal }, + ) // It's important to set the username during one of our first fetches this.plugin.setPluginState({ username }) currentPagePath = nextPagePath currentItems = currentItems.concat(items) - this.setLoading(this.plugin.uppy.i18n('loadedXFiles', { numFiles: items.length })) + this.setLoading( + this.plugin.uppy.i18n('loadedXFiles', { numFiles: items.length }), + ) } while (this.opts.loadAllFiles && currentPagePath) - const newPartialTree = PartialTreeUtils.afterOpenFolder(partialTree, currentItems, clickedFolder, currentPagePath, this.validateSingleFile) + const newPartialTree = PartialTreeUtils.afterOpenFolder( + partialTree, + currentItems, + clickedFolder, + currentPagePath, + this.validateSingleFile, + ) this.plugin.setPluginState({ partialTree: newPartialTree, currentFolderId: folderId, - searchString: '' + searchString: '', }) }).catch(handleError(this.plugin.uppy)) @@ -243,7 +271,7 @@ export default class ProviderView{ this.plugin.setPluginState({ ...getDefaultState(this.plugin.rootFolderId), - authenticated: false + authenticated: false, }) } }).catch(handleError(this.plugin.uppy)) @@ -264,12 +292,27 @@ export default class ProviderView{ async handleScroll(event: Event): Promise { const { partialTree, currentFolderId } = this.plugin.getPluginState() - const currentFolder = partialTree.find((i) => i.id === currentFolderId) as PartialTreeFolder - if (shouldHandleScroll(event) && !this.isHandlingScroll && currentFolder.nextPagePath) { + const currentFolder = partialTree.find( + (i) => i.id === currentFolderId, + ) as PartialTreeFolder + if ( + shouldHandleScroll(event) && + !this.isHandlingScroll && + currentFolder.nextPagePath + ) { this.isHandlingScroll = true await this.#withAbort(async (signal) => { - const { nextPagePath, items } = await this.provider.list(currentFolder.nextPagePath, { signal }) - const newPartialTree = PartialTreeUtils.afterScrollFolder(partialTree, currentFolderId, items, nextPagePath, this.validateSingleFile) + const { nextPagePath, items } = await this.provider.list( + currentFolder.nextPagePath, + { signal }, + ) + const newPartialTree = PartialTreeUtils.afterScrollFolder( + partialTree, + currentFolderId, + items, + nextPagePath, + this.validateSingleFile, + ) this.plugin.setPluginState({ partialTree: newPartialTree }) }).catch(handleError(this.plugin.uppy)) @@ -277,8 +320,8 @@ export default class ProviderView{ } } - validateSingleFile = (file: CompanionFile) : string | null => { - const companionFile : ValidateableFile = remoteFileObjToLocal(file) + validateSingleFile = (file: CompanionFile): string | null => { + const companionFile: ValidateableFile = remoteFileObjToLocal(file) const result = this.plugin.uppy.validateSingleFile(companionFile) return result } @@ -292,11 +335,12 @@ export default class ProviderView{ const enrichedTree: PartialTree = await PartialTreeUtils.afterFill( partialTree, (path: PartialTreeId) => this.provider.list(path, { signal }), - this.validateSingleFile + this.validateSingleFile, ) // 2. Now that we know how many files there are - recheck aggregateRestrictions! - const aggregateRestrictionError = this.validateAggregateRestrictions(enrichedTree) + const aggregateRestrictionError = + this.validateAggregateRestrictions(enrichedTree) if (aggregateRestrictionError) { this.plugin.setPluginState({ partialTree: enrichedTree }) return @@ -312,54 +356,75 @@ export default class ProviderView{ this.setLoading(false) } - getBreadcrumbs = () : PartialTreeFolder[] => { + getBreadcrumbs = (): PartialTreeFolder[] => { const { partialTree, currentFolderId } = this.plugin.getPluginState() if (!currentFolderId) return [] - const breadcrumbs : PartialTreeFolder[] = [] - let parent = partialTree.find((folder) => folder.id === currentFolderId) as PartialTreeFolder + const breadcrumbs: PartialTreeFolder[] = [] + let parent = partialTree.find( + (folder) => folder.id === currentFolderId, + ) as PartialTreeFolder while (true) { breadcrumbs.push(parent) if (parent.type === 'root') break - parent = partialTree.find((folder) => folder.id === (parent as PartialTreeFolderNode).parentId) as PartialTreeFolder + parent = partialTree.find( + (folder) => folder.id === (parent as PartialTreeFolderNode).parentId, + ) as PartialTreeFolder } return breadcrumbs.toReversed() } - toggleCheckbox(ourItem: PartialTreeFolderNode | PartialTreeFile, isShiftKeyPressed: boolean) { + toggleCheckbox( + ourItem: PartialTreeFolderNode | PartialTreeFile, + isShiftKeyPressed: boolean, + ) { const { partialTree } = this.plugin.getPluginState() - const clickedRange = getClickedRange(ourItem.id, this.getDisplayedPartialTree(), isShiftKeyPressed, this.lastCheckbox) - const newPartialTree = PartialTreeUtils.afterToggleCheckbox(partialTree, clickedRange, this.validateSingleFile) + const clickedRange = getClickedRange( + ourItem.id, + this.getDisplayedPartialTree(), + isShiftKeyPressed, + this.lastCheckbox, + ) + const newPartialTree = PartialTreeUtils.afterToggleCheckbox( + partialTree, + clickedRange, + this.validateSingleFile, + ) this.plugin.setPluginState({ partialTree: newPartialTree }) this.lastCheckbox = ourItem.id } - getDisplayedPartialTree = () : (PartialTreeFile | PartialTreeFolderNode)[] => { - const { partialTree, currentFolderId, searchString } = this.plugin.getPluginState() - const inThisFolder = partialTree.filter((item) => item.type !== 'root' && item.parentId === currentFolderId) as (PartialTreeFile | PartialTreeFolderNode)[] - const filtered = searchString === '' - ? inThisFolder - : inThisFolder.filter((item) => item.data.name.toLowerCase().indexOf(searchString.toLowerCase()) !== -1) + getDisplayedPartialTree = (): (PartialTreeFile | PartialTreeFolderNode)[] => { + const { partialTree, currentFolderId, searchString } = + this.plugin.getPluginState() + const inThisFolder = partialTree.filter( + (item) => item.type !== 'root' && item.parentId === currentFolderId, + ) as (PartialTreeFile | PartialTreeFolderNode)[] + const filtered = + searchString === '' ? inThisFolder : ( + inThisFolder.filter( + (item) => + item.data.name.toLowerCase().indexOf(searchString.toLowerCase()) !== + -1, + ) + ) return filtered } validateAggregateRestrictions = (partialTree: PartialTree) => { - const checkedFiles = partialTree.filter((item) => - item.type === 'file' && item.status === 'checked' + const checkedFiles = partialTree.filter( + (item) => item.type === 'file' && item.status === 'checked', ) as PartialTreeFile[] const uppyFiles = checkedFiles.map((file) => file.data) return this.plugin.uppy.validateAggregateRestrictions(uppyFiles) } - render( - state: unknown, - viewOptions: RenderOpts = {} - ): JSX.Element { + render(state: unknown, viewOptions: RenderOpts = {}): JSX.Element { const { didFirstRender } = this.plugin.getPluginState() const { i18n } = this.plugin.uppy @@ -369,7 +434,7 @@ export default class ProviderView{ this.openFolder(this.plugin.rootFolderId) } - const opts : Opts = { ...this.opts, ...viewOptions } + const opts: Opts = { ...this.opts, ...viewOptions } const { authenticated, partialTree, username, searchString, loading } = this.plugin.getPluginState() const pluginIcon = this.plugin.icon || defaultPickerIcon @@ -387,58 +452,60 @@ export default class ProviderView{ ) } - return
    - - showBreadcrumbs={opts.showBreadcrumbs} - openFolder={this.openFolder} - breadcrumbs={this.getBreadcrumbs()} - pluginIcon={pluginIcon} - title={this.plugin.title} - logout={this.logout} - username={username} - i18n={i18n} - /> - - {opts.showFilter && ( - { - console.log('setting searchString!', searchString); - this.plugin.setPluginState({ searchString }) - }} - submitSearchString={() => {}} - inputLabel={i18n('filter')} - clearSearchLabel={i18n('resetFilter')} - wrapperClassName="uppy-ProviderBrowser-searchFilter" - inputClassName="uppy-ProviderBrowser-searchFilterInput" + return ( +
    + + showBreadcrumbs={opts.showBreadcrumbs} + openFolder={this.openFolder} + breadcrumbs={this.getBreadcrumbs()} + pluginIcon={pluginIcon} + title={this.plugin.title} + logout={this.logout} + username={username} + i18n={i18n} /> - )} - - - toggleCheckbox={this.toggleCheckbox} - displayedPartialTree={this.getDisplayedPartialTree()} - openFolder={this.openFolder} - loadAllFiles={opts.loadAllFiles} - noResultsLabel={i18n('noFilesFound')} - handleScroll={this.handleScroll} - viewType={opts.viewType} - showTitles={opts.showTitles} - i18n={this.plugin.uppy.i18n} - isLoading={loading} - /> - - -
    + + {opts.showFilter && ( + { + console.log('setting searchString!', searchString) + this.plugin.setPluginState({ searchString }) + }} + submitSearchString={() => {}} + inputLabel={i18n('filter')} + clearSearchLabel={i18n('resetFilter')} + wrapperClassName="uppy-ProviderBrowser-searchFilter" + inputClassName="uppy-ProviderBrowser-searchFilterInput" + /> + )} + + + toggleCheckbox={this.toggleCheckbox} + displayedPartialTree={this.getDisplayedPartialTree()} + openFolder={this.openFolder} + loadAllFiles={opts.loadAllFiles} + noResultsLabel={i18n('noFilesFound')} + handleScroll={this.handleScroll} + viewType={opts.viewType} + showTitles={opts.showTitles} + i18n={this.plugin.uppy.i18n} + isLoading={loading} + /> + + +
    + ) } } diff --git a/packages/@uppy/provider-views/src/ProviderView/User.tsx b/packages/@uppy/provider-views/src/ProviderView/User.tsx index fdc071c3be..53dab79c53 100644 --- a/packages/@uppy/provider-views/src/ProviderView/User.tsx +++ b/packages/@uppy/provider-views/src/ProviderView/User.tsx @@ -9,12 +9,11 @@ type UserProps = { export default function User({ i18n, logout, username }: UserProps) { return ( - { - username && + {username && ( {username} - } + )}
    + // label for a checkbox + : } diff --git a/packages/@uppy/provider-views/src/Item/index.tsx b/packages/@uppy/provider-views/src/Item/index.tsx index 0d338f4d4d..8fbf88cbd5 100644 --- a/packages/@uppy/provider-views/src/Item/index.tsx +++ b/packages/@uppy/provider-views/src/Item/index.tsx @@ -8,32 +8,30 @@ import type { PartialTreeFolderNode, PartialTreeId, } from '@uppy/core/lib/Uppy.ts' -import ItemIcon from './components/ItemIcon.tsx' import GridItem from './components/GridItem.tsx' import ListItem from './components/ListItem.tsx' type ItemProps = { - viewType: string + file: PartialTreeFile | PartialTreeFolderNode + openFolder: (folderId: PartialTreeId) => void toggleCheckbox: (event: Event) => void + viewType: string showTitles: boolean i18n: I18n - openFolder: (folderId: PartialTreeId) => void - file: PartialTreeFile | PartialTreeFolderNode } export default function Item(props: ItemProps): h.JSX.Element { const { viewType, toggleCheckbox, showTitles, i18n, openFolder, file } = props const restrictionError = file.type === 'folder' ? null : file.restrictionError - const isDisabled = Boolean(restrictionError) && file.status !== 'checked' + const isDisabled = !!restrictionError && file.status !== 'checked' - const sharedProps = { - id: file.id, - title: file.data.name, - status: file.status, + const ourProps = { + file, + openFolder, + toggleCheckbox, i18n, - toggleCheckbox, viewType, showTitles, className: classNames( @@ -43,23 +41,10 @@ export default function Item(props: ItemProps): h.JSX.Element { { 'uppy-ProviderBrowserItem--is-checked': file.status === 'checked' }, { 'uppy-ProviderBrowserItem--is-partial': file.status === 'partial' }, ), - itemIconEl: , isDisabled, restrictionError, } - const ourProps = - file.data.isFolder ? - { - ...sharedProps, - type: 'folder', - handleFolderClick: () => openFolder(file.id), - } - : { - ...sharedProps, - type: 'file', - } - switch (viewType) { case 'grid': return From 294a301214b3efdad09824edcc8e35da09e4a9e6 Mon Sep 17 00:00:00 2001 From: Evgenia Karunus Date: Thu, 6 Jun 2024 14:17:31 +0400 Subject: [PATCH 144/170] - disable checkboxes for GoogleDrive team drives See #5232 --- .../src/Item/components/ListItem.tsx | 43 +++++++++++-------- 1 file changed, 26 insertions(+), 17 deletions(-) diff --git a/packages/@uppy/provider-views/src/Item/components/ListItem.tsx b/packages/@uppy/provider-views/src/Item/components/ListItem.tsx index 27886a0d1e..f5f023bba2 100644 --- a/packages/@uppy/provider-views/src/Item/components/ListItem.tsx +++ b/packages/@uppy/provider-views/src/Item/components/ListItem.tsx @@ -33,27 +33,36 @@ export default function ListItem({ showTitles, i18n, }: ListItemProps): h.JSX.Element { + // Disable checkboxes for GoogleDrive's team drives (github.com/transloadit/uppy/issues/5232) + const isCheckboxHidden = file.data.custom?.isSharedDrive + return (
  • - - name="listitem" - id={file.id} - checked={file.status === 'checked'} - aria-label={ - file.data.isFolder ? - i18n('allFilesFromFolderNamed', { name: file.data.name }) - : null - } - disabled={isDisabled} - data-uppy-super-focusable - /> + {!isCheckboxHidden && ( + + name="listitem" + id={file.id} + checked={file.status === 'checked'} + aria-label={ + file.data.isFolder ? + i18n('allFilesFromFolderNamed', { name: file.data.name }) + : null + } + disabled={isDisabled} + data-uppy-super-focusable + /> + )} { file.data.isFolder ? From a65dae3102a5cb1ff6da75904ef874affe4c591a Mon Sep 17 00:00:00 2001 From: Evgenia Karunus Date: Thu, 6 Jun 2024 14:42:42 +0400 Subject: [PATCH 145/170] merge (fixing up some lines from the previous merge) --- packages/@uppy/instagram/src/Instagram.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/@uppy/instagram/src/Instagram.tsx b/packages/@uppy/instagram/src/Instagram.tsx index 6a5580a4c0..284794d6f8 100644 --- a/packages/@uppy/instagram/src/Instagram.tsx +++ b/packages/@uppy/instagram/src/Instagram.tsx @@ -72,7 +72,6 @@ export default class Instagram extends UIPlugin< ) - this.rootFolderId = 'recent' this.defaultLocale = locale From 258c379caa787b7d3d751a03913ba3fabfc057e4 Mon Sep 17 00:00:00 2001 From: Evgenia Karunus Date: Thu, 6 Jun 2024 15:13:34 +0400 Subject: [PATCH 146/170] merge (fixing up some lines from the previous merge) --- packages/@uppy/google-drive/src/GoogleDrive.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/@uppy/google-drive/src/GoogleDrive.tsx b/packages/@uppy/google-drive/src/GoogleDrive.tsx index 9e4561c0d9..b7db457bc6 100644 --- a/packages/@uppy/google-drive/src/GoogleDrive.tsx +++ b/packages/@uppy/google-drive/src/GoogleDrive.tsx @@ -77,7 +77,6 @@ export default class GoogleDrive< ) - this.rootFolderId = 'root' this.opts.companionAllowedHosts = getAllowedHosts( this.opts.companionAllowedHosts, From b54d12e49acc7a3dfd0935df001326855ffe4a12 Mon Sep 17 00:00:00 2001 From: Evgenia Karunus Date: Thu, 6 Jun 2024 15:20:56 +0400 Subject: [PATCH 147/170] everywhere - remove TEMP development values --- packages/@uppy/companion/src/server/provider/drive/index.js | 2 +- .../companion/src/server/provider/instagram/graph/index.js | 2 +- packages/@uppy/google-drive/src/GoogleDrive.tsx | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/@uppy/companion/src/server/provider/drive/index.js b/packages/@uppy/companion/src/server/provider/drive/index.js index 2dc3c92bd9..5c2b4ff327 100644 --- a/packages/@uppy/companion/src/server/provider/drive/index.js +++ b/packages/@uppy/companion/src/server/provider/drive/index.js @@ -97,7 +97,7 @@ class Drive extends Provider { q, // We can only do a page size of 1000 because we do not request permissions in DRIVE_FILES_FIELDS. // Otherwise we are limited to 100. Instead we get the user info from `this.user()` - pageSize: 10, + pageSize: 1000, orderBy: 'folder,name', includeItemsFromAllDrives: true, supportsAllDrives: true, diff --git a/packages/@uppy/companion/src/server/provider/instagram/graph/index.js b/packages/@uppy/companion/src/server/provider/instagram/graph/index.js index 4b85fa0679..db124452df 100644 --- a/packages/@uppy/companion/src/server/provider/instagram/graph/index.js +++ b/packages/@uppy/companion/src/server/provider/instagram/graph/index.js @@ -37,7 +37,7 @@ class Instagram extends Provider { async list ({ directory, token, query = { cursor: null } }) { return this.#withErrorHandling('provider.instagram.list.error', async () => { - const qs = { fields: 'id,media_type,thumbnail_url,media_url,timestamp,children{media_type,media_url,thumbnail_url,timestamp}', limit: 5 } + const qs = { fields: 'id,media_type,thumbnail_url,media_url,timestamp,children{media_type,media_url,thumbnail_url,timestamp}' } if (query.cursor) qs.after = query.cursor diff --git a/packages/@uppy/google-drive/src/GoogleDrive.tsx b/packages/@uppy/google-drive/src/GoogleDrive.tsx index b7db457bc6..fc6a0a9f79 100644 --- a/packages/@uppy/google-drive/src/GoogleDrive.tsx +++ b/packages/@uppy/google-drive/src/GoogleDrive.tsx @@ -103,7 +103,7 @@ export default class GoogleDrive< install(): void { this.view = new ProviderViews(this, { provider: this.provider, - loadAllFiles: false, + loadAllFiles: true, }) const { target } = this.opts From 464f61414472dfe5339e546c67f9b161e067f03a Mon Sep 17 00:00:00 2001 From: Evgenia Karunus Date: Fri, 7 Jun 2024 14:54:08 +0400 Subject: [PATCH 148/170] `this.validateSingleFile()` - switch to `.restrictionError` --- .../src/ProviderView/ProviderView.tsx | 1 - .../SearchProviderView/SearchProviderView.tsx | 1 - .../PartialTreeUtils/afterToggleCheckbox.ts | 20 ++++++------- .../src/utils/PartialTreeUtils/index.test.ts | 28 ++++++++----------- 4 files changed, 22 insertions(+), 28 deletions(-) diff --git a/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx b/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx index cfe76adf85..9f3e5301fe 100644 --- a/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx +++ b/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx @@ -366,7 +366,6 @@ export default class ProviderView { const newPartialTree = PartialTreeUtils.afterToggleCheckbox( partialTree, clickedRange, - this.validateSingleFile, ) this.plugin.setPluginState({ partialTree: newPartialTree }) diff --git a/packages/@uppy/provider-views/src/SearchProviderView/SearchProviderView.tsx b/packages/@uppy/provider-views/src/SearchProviderView/SearchProviderView.tsx index 58ad9a77af..48b0e57253 100644 --- a/packages/@uppy/provider-views/src/SearchProviderView/SearchProviderView.tsx +++ b/packages/@uppy/provider-views/src/SearchProviderView/SearchProviderView.tsx @@ -247,7 +247,6 @@ export default class SearchProviderView { const newPartialTree = PartialTreeUtils.afterToggleCheckbox( partialTree, clickedRange, - this.validateSingleFile, ) this.plugin.setPluginState({ partialTree: newPartialTree }) diff --git a/packages/@uppy/provider-views/src/utils/PartialTreeUtils/afterToggleCheckbox.ts b/packages/@uppy/provider-views/src/utils/PartialTreeUtils/afterToggleCheckbox.ts index 35edb1f9b1..d95a114798 100644 --- a/packages/@uppy/provider-views/src/utils/PartialTreeUtils/afterToggleCheckbox.ts +++ b/packages/@uppy/provider-views/src/utils/PartialTreeUtils/afterToggleCheckbox.ts @@ -5,13 +5,11 @@ import type { PartialTreeFolder, PartialTreeFolderNode, } from '@uppy/core/lib/Uppy' -import type { CompanionFile } from '@uppy/utils/lib/CompanionFile' import clone from './clone' const afterToggleCheckbox = ( oldPartialTree: PartialTree, clickedRange: string[], - validateSingleFile: (file: CompanionFile) => string | null, ): PartialTree => { const newPartialTree: PartialTree = clone(oldPartialTree) const ourItem = newPartialTree.find((item) => item.id === clickedRange[0]) as @@ -37,7 +35,7 @@ const afterToggleCheckbox = ( percolateDown(item, isParentFolderChecked) }) } - // we do something to all of its parents. + const percolateUp = ( currentItem: PartialTreeFolderNode | PartialTreeFile, ) => { @@ -46,16 +44,18 @@ const afterToggleCheckbox = ( )! as PartialTreeFolder if (parentFolder.type === 'root') return - const parentsChildren = newPartialTree.filter( - (item) => item.type !== 'root' && item.parentId === parentFolder.id, + const validChildren = newPartialTree.filter( + (item) => + // is a child + item.type !== 'root' && + item.parentId === parentFolder.id && + // does pass validations + !(item.type === 'file' && item.restrictionError), ) as (PartialTreeFile | PartialTreeFolderNode)[] - const parentsValidChildren = parentsChildren.filter( - (item) => !validateSingleFile(item.data), - ) - const areAllChildrenChecked = parentsValidChildren.every( + const areAllChildrenChecked = validChildren.every( (item) => item.status === 'checked', ) - const areAllChildrenUnchecked = parentsValidChildren.every( + const areAllChildrenUnchecked = validChildren.every( (item) => item.status === 'unchecked', ) diff --git a/packages/@uppy/provider-views/src/utils/PartialTreeUtils/index.test.ts b/packages/@uppy/provider-views/src/utils/PartialTreeUtils/index.test.ts index df5d5c2016..6cf741df12 100644 --- a/packages/@uppy/provider-views/src/utils/PartialTreeUtils/index.test.ts +++ b/packages/@uppy/provider-views/src/utils/PartialTreeUtils/index.test.ts @@ -393,7 +393,7 @@ describe('afterToggleCheckbox()', () => { ] it('check folder: percolates up and down', () => { - const newTree = afterToggleCheckbox(oldPartialTree, ['2_4'], () => null) + const newTree = afterToggleCheckbox(oldPartialTree, ['2_4']) expect(getFolder(newTree, '2_4').status).toEqual('checked') // percolates down @@ -405,13 +405,9 @@ describe('afterToggleCheckbox()', () => { }) it('uncheck folder: percolates up and down', () => { - const treeAfterClick1 = afterToggleCheckbox( - oldPartialTree, - ['2_4'], - () => null, - ) + const treeAfterClick1 = afterToggleCheckbox(oldPartialTree, ['2_4']) - const tree = afterToggleCheckbox(treeAfterClick1, ['2_4'], () => null) + const tree = afterToggleCheckbox(treeAfterClick1, ['2_4']) expect(getFolder(tree, '2_4').status).toEqual('unchecked') // percolates down @@ -423,11 +419,11 @@ describe('afterToggleCheckbox()', () => { }) it('gradually check all subfolders: marks parent folder as checked', () => { - const tree = afterToggleCheckbox( - oldPartialTree, - ['2_4_1', '2_4_2', '2_4_3'], - () => null, - ) + const tree = afterToggleCheckbox(oldPartialTree, [ + '2_4_1', + '2_4_2', + '2_4_3', + ]) // marks children as checked expect(getFolder(tree, '2_4_1').status).toEqual('checked') @@ -444,14 +440,14 @@ describe('afterToggleCheckbox()', () => { it('clicking partial folder: partial => checked => unchecked', () => { // 1. click on 2_4_1, thus making 2_4 "partial" - const tree_1 = afterToggleCheckbox(oldPartialTree, ['2_4_1'], () => null) + const tree_1 = afterToggleCheckbox(oldPartialTree, ['2_4_1']) expect(getFolder(tree_1, '2_4').status).toEqual('partial') // and test children while we're at it expect(getFolder(tree_1, '2_4_1').status).toEqual('checked') // 2. click on 2_4, thus making 2_4 "checked" - const tree_2 = afterToggleCheckbox(tree_1, ['2_4'], () => null) + const tree_2 = afterToggleCheckbox(tree_1, ['2_4']) expect(getFolder(tree_2, '2_4').status).toEqual('checked') // and test children while we're at it @@ -460,7 +456,7 @@ describe('afterToggleCheckbox()', () => { expect(getFolder(tree_2, '2_4_3').status).toEqual('checked') // 3. click on 2_4, thus making 2_4 "unchecked" - const tree_3 = afterToggleCheckbox(tree_2, ['2_4'], () => null) + const tree_3 = afterToggleCheckbox(tree_2, ['2_4']) expect(getFolder(tree_3, '2_4').status).toEqual('unchecked') // and test children while we're at it @@ -471,7 +467,7 @@ describe('afterToggleCheckbox()', () => { it('old partialTree is NOT mutated', () => { const oldPartialTreeCopy = JSON.parse(JSON.stringify(oldPartialTree)) - afterToggleCheckbox(oldPartialTree, ['2_4_1'], () => null) + afterToggleCheckbox(oldPartialTree, ['2_4_1']) expect(oldPartialTree).toEqual(oldPartialTreeCopy) }) }) From c94b893a9777af04530efb4d22d634a155897c98 Mon Sep 17 00:00:00 2001 From: Evgenia Karunus Date: Fri, 7 Jun 2024 15:52:40 +0400 Subject: [PATCH 149/170] `afterToggleCheckbox.ts` - refactor, add comments --- .../PartialTreeUtils/afterToggleCheckbox.ts | 157 ++++++++++-------- 1 file changed, 91 insertions(+), 66 deletions(-) diff --git a/packages/@uppy/provider-views/src/utils/PartialTreeUtils/afterToggleCheckbox.ts b/packages/@uppy/provider-views/src/utils/PartialTreeUtils/afterToggleCheckbox.ts index d95a114798..4ae9057b9e 100644 --- a/packages/@uppy/provider-views/src/utils/PartialTreeUtils/afterToggleCheckbox.ts +++ b/packages/@uppy/provider-views/src/utils/PartialTreeUtils/afterToggleCheckbox.ts @@ -7,69 +7,94 @@ import type { } from '@uppy/core/lib/Uppy' import clone from './clone' -const afterToggleCheckbox = ( - oldPartialTree: PartialTree, - clickedRange: string[], -): PartialTree => { - const newPartialTree: PartialTree = clone(oldPartialTree) - const ourItem = newPartialTree.find((item) => item.id === clickedRange[0]) as - | PartialTreeFile - | PartialTreeFolderNode +/* + FROM | TO - const percolateDown = ( - clickedItem: PartialTreeFolderNode | PartialTreeFile, - isParentFolderChecked: boolean, - ) => { - const children = newPartialTree.filter( - (item) => item.type !== 'root' && item.parentId === clickedItem.id, - ) as (PartialTreeFolderNode | PartialTreeFile)[] - children.forEach((item) => { - if (item.type === 'file') { - item.status = - isParentFolderChecked && !item.restrictionError ? - 'checked' - : 'unchecked' - } else { - item.status = isParentFolderChecked ? 'checked' : 'unchecked' - } - percolateDown(item, isParentFolderChecked) - }) - } + root | root + folder | folder + folder ✅︎ | folder ✅︎ + file | file ✅︎ + file | file ✅︎ + folder | folder ✅︎ + file | file ✅︎ + file | file + file | file +*/ +const percolateDown = ( + tree: PartialTree, + clickedItem: PartialTreeFolderNode | PartialTreeFile, + isParentFolderChecked: boolean, +) => { + const children = tree.filter( + (item) => item.type !== 'root' && item.parentId === clickedItem.id, + ) as (PartialTreeFolderNode | PartialTreeFile)[] + children.forEach((item) => { + if (item.type === 'file') { + item.status = + isParentFolderChecked && !item.restrictionError ? + 'checked' + : 'unchecked' + } else { + item.status = isParentFolderChecked ? 'checked' : 'unchecked' + } + percolateDown(tree, item, isParentFolderChecked) + }) +} - const percolateUp = ( - currentItem: PartialTreeFolderNode | PartialTreeFile, - ) => { - const parentFolder = newPartialTree.find( - (item) => item.id === currentItem.parentId, - )! as PartialTreeFolder - if (parentFolder.type === 'root') return +/* + FROM | TO - const validChildren = newPartialTree.filter( - (item) => - // is a child - item.type !== 'root' && - item.parentId === parentFolder.id && - // does pass validations - !(item.type === 'file' && item.restrictionError), - ) as (PartialTreeFile | PartialTreeFolderNode)[] - const areAllChildrenChecked = validChildren.every( - (item) => item.status === 'checked', - ) - const areAllChildrenUnchecked = validChildren.every( - (item) => item.status === 'unchecked', - ) + root | root + folder | folder + folder | folder [▬] ('partial' status) + file | file + folder | folder ✅︎ + file ✅︎ | file ✅︎ + file | file + file | file +*/ +const percolateUp = ( + tree: PartialTree, + currentItem: PartialTreeFolderNode | PartialTreeFile, +) => { + const parentFolder = tree.find( + (item) => item.id === currentItem.parentId, + )! as PartialTreeFolder + if (parentFolder.type === 'root') return - if (areAllChildrenChecked) { - parentFolder.status = 'checked' - } else if (areAllChildrenUnchecked) { - parentFolder.status = 'unchecked' - } else { - parentFolder.status = 'partial' - } + const validChildren = tree.filter( + (item) => + // is a child + item.type !== 'root' && + item.parentId === parentFolder.id && + // does pass validations + !(item.type === 'file' && item.restrictionError), + ) as (PartialTreeFile | PartialTreeFolderNode)[] + const areAllChildrenChecked = validChildren.every( + (item) => item.status === 'checked', + ) + const areAllChildrenUnchecked = validChildren.every( + (item) => item.status === 'unchecked', + ) - percolateUp(parentFolder) + if (areAllChildrenChecked) { + parentFolder.status = 'checked' + } else if (areAllChildrenUnchecked) { + parentFolder.status = 'unchecked' + } else { + parentFolder.status = 'partial' } + percolateUp(tree, parentFolder) +} + +const afterToggleCheckbox = ( + oldPartialTree: PartialTree, + clickedRange: string[], +): PartialTree => { + const newPartialTree: PartialTree = clone(oldPartialTree) + + // We checked two or more items if (clickedRange.length >= 2) { const newlyCheckedItems = newPartialTree.filter( (item) => item.type !== 'root' && clickedRange.includes(item.id), @@ -84,18 +109,18 @@ const afterToggleCheckbox = ( }) newlyCheckedItems.forEach((item) => { - percolateDown(item, true) + percolateDown(newPartialTree, item, true) }) - percolateUp(ourItem) + percolateUp(newPartialTree, newlyCheckedItems[0]) + // We checked exactly one item } else { - const oldStatus = ( - oldPartialTree.find((item) => item.id === clickedRange[0]) as - | PartialTreeFile - | PartialTreeFolderNode - ).status - ourItem.status = oldStatus === 'checked' ? 'unchecked' : 'checked' - percolateDown(ourItem, ourItem.status === 'checked') - percolateUp(ourItem) + const clickedItem = newPartialTree.find( + (item) => item.id === clickedRange[0], + ) as PartialTreeFile | PartialTreeFolderNode + clickedItem.status = + clickedItem.status === 'checked' ? 'unchecked' : 'checked' + percolateDown(newPartialTree, clickedItem, clickedItem.status === 'checked') + percolateUp(newPartialTree, clickedItem) } return newPartialTree From ab6a13cabe9455f302ca0a847663cff8770b350c Mon Sep 17 00:00:00 2001 From: Evgenia Karunus Date: Fri, 7 Jun 2024 16:12:51 +0400 Subject: [PATCH 150/170] `afterToggleCheckbox.ts` - refactor to use ids instead of whole objects --- .../PartialTreeUtils/afterToggleCheckbox.ts | 55 +++++++++---------- 1 file changed, 26 insertions(+), 29 deletions(-) diff --git a/packages/@uppy/provider-views/src/utils/PartialTreeUtils/afterToggleCheckbox.ts b/packages/@uppy/provider-views/src/utils/PartialTreeUtils/afterToggleCheckbox.ts index 4ae9057b9e..c6aa7d73e9 100644 --- a/packages/@uppy/provider-views/src/utils/PartialTreeUtils/afterToggleCheckbox.ts +++ b/packages/@uppy/provider-views/src/utils/PartialTreeUtils/afterToggleCheckbox.ts @@ -4,6 +4,7 @@ import type { PartialTreeFile, PartialTreeFolder, PartialTreeFolderNode, + PartialTreeId, } from '@uppy/core/lib/Uppy' import clone from './clone' @@ -22,22 +23,18 @@ import clone from './clone' */ const percolateDown = ( tree: PartialTree, - clickedItem: PartialTreeFolderNode | PartialTreeFile, - isParentFolderChecked: boolean, + id: PartialTreeId, + shouldMarkAsChecked: boolean, ) => { const children = tree.filter( - (item) => item.type !== 'root' && item.parentId === clickedItem.id, + (item) => item.type !== 'root' && item.parentId === id, ) as (PartialTreeFolderNode | PartialTreeFile)[] children.forEach((item) => { - if (item.type === 'file') { - item.status = - isParentFolderChecked && !item.restrictionError ? - 'checked' - : 'unchecked' - } else { - item.status = isParentFolderChecked ? 'checked' : 'unchecked' - } - percolateDown(tree, item, isParentFolderChecked) + item.status = + shouldMarkAsChecked && !(item.type === 'file' && item.restrictionError) ? + 'checked' + : 'unchecked' + percolateDown(tree, item.id, shouldMarkAsChecked) }) } @@ -53,23 +50,19 @@ const percolateDown = ( file | file file | file */ -const percolateUp = ( - tree: PartialTree, - currentItem: PartialTreeFolderNode | PartialTreeFile, -) => { - const parentFolder = tree.find( - (item) => item.id === currentItem.parentId, - )! as PartialTreeFolder - if (parentFolder.type === 'root') return +const percolateUp = (tree: PartialTree, id: PartialTreeId) => { + const folder = tree.find((item) => item.id === id) as PartialTreeFolder + if (folder.type === 'root') return const validChildren = tree.filter( (item) => // is a child item.type !== 'root' && - item.parentId === parentFolder.id && + item.parentId === folder.id && // does pass validations !(item.type === 'file' && item.restrictionError), ) as (PartialTreeFile | PartialTreeFolderNode)[] + const areAllChildrenChecked = validChildren.every( (item) => item.status === 'checked', ) @@ -78,14 +71,14 @@ const percolateUp = ( ) if (areAllChildrenChecked) { - parentFolder.status = 'checked' + folder.status = 'checked' } else if (areAllChildrenUnchecked) { - parentFolder.status = 'unchecked' + folder.status = 'unchecked' } else { - parentFolder.status = 'partial' + folder.status = 'partial' } - percolateUp(tree, parentFolder) + percolateUp(tree, folder.parentId) } const afterToggleCheckbox = ( @@ -109,9 +102,9 @@ const afterToggleCheckbox = ( }) newlyCheckedItems.forEach((item) => { - percolateDown(newPartialTree, item, true) + percolateDown(newPartialTree, item.id, true) }) - percolateUp(newPartialTree, newlyCheckedItems[0]) + percolateUp(newPartialTree, newlyCheckedItems[0].parentId) // We checked exactly one item } else { const clickedItem = newPartialTree.find( @@ -119,8 +112,12 @@ const afterToggleCheckbox = ( ) as PartialTreeFile | PartialTreeFolderNode clickedItem.status = clickedItem.status === 'checked' ? 'unchecked' : 'checked' - percolateDown(newPartialTree, clickedItem, clickedItem.status === 'checked') - percolateUp(newPartialTree, clickedItem) + percolateDown( + newPartialTree, + clickedItem.id, + clickedItem.status === 'checked', + ) + percolateUp(newPartialTree, clickedItem.parentId) } return newPartialTree From 55af6d85c4b6fb7bc1a91d8b5d2b313e65dbc2f2 Mon Sep 17 00:00:00 2001 From: Evgenia Karunus Date: Fri, 7 Jun 2024 16:15:14 +0400 Subject: [PATCH 151/170] `afterToggleCheckbox.ts` - try to satisfy prettier --- .../PartialTreeUtils/afterToggleCheckbox.ts | 30 ++++++++----------- 1 file changed, 13 insertions(+), 17 deletions(-) diff --git a/packages/@uppy/provider-views/src/utils/PartialTreeUtils/afterToggleCheckbox.ts b/packages/@uppy/provider-views/src/utils/PartialTreeUtils/afterToggleCheckbox.ts index c6aa7d73e9..e399988e37 100644 --- a/packages/@uppy/provider-views/src/utils/PartialTreeUtils/afterToggleCheckbox.ts +++ b/packages/@uppy/provider-views/src/utils/PartialTreeUtils/afterToggleCheckbox.ts @@ -82,14 +82,14 @@ const percolateUp = (tree: PartialTree, id: PartialTreeId) => { } const afterToggleCheckbox = ( - oldPartialTree: PartialTree, + oldTree: PartialTree, clickedRange: string[], ): PartialTree => { - const newPartialTree: PartialTree = clone(oldPartialTree) + const tree: PartialTree = clone(oldTree) - // We checked two or more items if (clickedRange.length >= 2) { - const newlyCheckedItems = newPartialTree.filter( + // We checked two or more items + const newlyCheckedItems = tree.filter( (item) => item.type !== 'root' && clickedRange.includes(item.id), ) as (PartialTreeFile | PartialTreeFolderNode)[] @@ -102,25 +102,21 @@ const afterToggleCheckbox = ( }) newlyCheckedItems.forEach((item) => { - percolateDown(newPartialTree, item.id, true) + percolateDown(tree, item.id, true) }) - percolateUp(newPartialTree, newlyCheckedItems[0].parentId) - // We checked exactly one item + percolateUp(tree, newlyCheckedItems[0].parentId) } else { - const clickedItem = newPartialTree.find( - (item) => item.id === clickedRange[0], - ) as PartialTreeFile | PartialTreeFolderNode + // We checked exactly one item + const clickedItem = tree.find((item) => item.id === clickedRange[0]) as + | PartialTreeFile + | PartialTreeFolderNode clickedItem.status = clickedItem.status === 'checked' ? 'unchecked' : 'checked' - percolateDown( - newPartialTree, - clickedItem.id, - clickedItem.status === 'checked', - ) - percolateUp(newPartialTree, clickedItem.parentId) + percolateDown(tree, clickedItem.id, clickedItem.status === 'checked') + percolateUp(tree, clickedItem.parentId) } - return newPartialTree + return tree } export default afterToggleCheckbox From 657ef98a384e18534f166d03d74e922a7585fb12 Mon Sep 17 00:00:00 2001 From: Evgenia Karunus Date: Wed, 12 Jun 2024 18:16:26 +0400 Subject: [PATCH 152/170] fixing 10 (try to satisfy `npx webpack`) --- .../provider-views/src/utils/PartialTreeUtils/index.ts | 8 ++++---- packages/@uppy/provider-views/src/utils/addFiles.ts | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/@uppy/provider-views/src/utils/PartialTreeUtils/index.ts b/packages/@uppy/provider-views/src/utils/PartialTreeUtils/index.ts index 1458bfd9d5..75af88923f 100644 --- a/packages/@uppy/provider-views/src/utils/PartialTreeUtils/index.ts +++ b/packages/@uppy/provider-views/src/utils/PartialTreeUtils/index.ts @@ -1,7 +1,7 @@ -import afterOpenFolder from './afterOpenFolder' -import afterScrollFolder from './afterScrollFolder' -import afterToggleCheckbox from './afterToggleCheckbox' -import afterFill from './afterFill' +import afterOpenFolder from './afterOpenFolder.ts' +import afterScrollFolder from './afterScrollFolder.ts' +import afterToggleCheckbox from './afterToggleCheckbox.ts' +import afterFill from './afterFill.ts' export default { afterOpenFolder, diff --git a/packages/@uppy/provider-views/src/utils/addFiles.ts b/packages/@uppy/provider-views/src/utils/addFiles.ts index 39a1190e4c..f5b4939451 100644 --- a/packages/@uppy/provider-views/src/utils/addFiles.ts +++ b/packages/@uppy/provider-views/src/utils/addFiles.ts @@ -6,7 +6,7 @@ import type { import type { CompanionFile } from '@uppy/utils/lib/CompanionFile' import type { Meta, Body, TagFile } from '@uppy/utils/lib/UppyFile' import { getSafeFileId } from '@uppy/utils/lib/generateFileID' -import getTagFile from './getTagFile' +import getTagFile from './getTagFile.ts' const addFiles = ( companionFiles: CompanionFile[], From 4081f06520ec4ec446e35a40df99b23c415a1896 Mon Sep 17 00:00:00 2001 From: Evgenia Karunus Date: Wed, 12 Jun 2024 18:21:03 +0400 Subject: [PATCH 153/170] fixing 11 (try to satisfy `npx webpack`) --- .../provider-views/src/utils/PartialTreeUtils/afterFill.ts | 2 +- .../src/utils/PartialTreeUtils/afterToggleCheckbox.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/@uppy/provider-views/src/utils/PartialTreeUtils/afterFill.ts b/packages/@uppy/provider-views/src/utils/PartialTreeUtils/afterFill.ts index ec7ef2d2f1..4e1ef142dd 100644 --- a/packages/@uppy/provider-views/src/utils/PartialTreeUtils/afterFill.ts +++ b/packages/@uppy/provider-views/src/utils/PartialTreeUtils/afterFill.ts @@ -7,7 +7,7 @@ import type { } from '@uppy/core/lib/Uppy' import type { CompanionFile } from '@uppy/utils/lib/CompanionFile' import PQueue from 'p-queue' -import clone from './clone' +import clone from './clone.ts' export interface ApiList { (directory: PartialTreeId): Promise<{ diff --git a/packages/@uppy/provider-views/src/utils/PartialTreeUtils/afterToggleCheckbox.ts b/packages/@uppy/provider-views/src/utils/PartialTreeUtils/afterToggleCheckbox.ts index e399988e37..bc4b6ccd8e 100644 --- a/packages/@uppy/provider-views/src/utils/PartialTreeUtils/afterToggleCheckbox.ts +++ b/packages/@uppy/provider-views/src/utils/PartialTreeUtils/afterToggleCheckbox.ts @@ -6,7 +6,7 @@ import type { PartialTreeFolderNode, PartialTreeId, } from '@uppy/core/lib/Uppy' -import clone from './clone' +import clone from './clone.ts' /* FROM | TO From 452800b0cbc72a2b6405863fcac852dc15b8f45d Mon Sep 17 00:00:00 2001 From: Evgenia Karunus Date: Thu, 13 Jun 2024 19:17:19 +0400 Subject: [PATCH 154/170] Antoine: use Math.min & Math.max in `getClickedRange()` Co-authored-by: Antoine du Hamel --- .../@uppy/provider-views/src/utils/getClickedRange.ts | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/packages/@uppy/provider-views/src/utils/getClickedRange.ts b/packages/@uppy/provider-views/src/utils/getClickedRange.ts index 450d46b3a5..660b5b6054 100644 --- a/packages/@uppy/provider-views/src/utils/getClickedRange.ts +++ b/packages/@uppy/provider-views/src/utils/getClickedRange.ts @@ -19,13 +19,10 @@ const getClickedRange = ( const newCheckboxIndex = displayedPartialTree.findIndex( (item) => item.id === clickedId, ) - const clickedRange = ( - lastCheckboxIndex < newCheckboxIndex ? - displayedPartialTree.slice(lastCheckboxIndex, newCheckboxIndex + 1) - : displayedPartialTree.slice( - newCheckboxIndex, - lastCheckboxIndex + 1, - )).map((item) => item.id) + const clickedRange = displayedPartialTree.slice( + Math.min(lastCheckboxIndex, newCheckboxIndex), + Math.max(lastCheckboxIndex, newCheckboxIndex) + 1, + ).map((item) => item.id) return clickedRange } From 3651e0c0de11eea08441e57713a0b327702accf4 Mon Sep 17 00:00:00 2001 From: Evgenia Karunus Date: Thu, 13 Jun 2024 19:24:28 +0400 Subject: [PATCH 155/170] fixing 12 (run `yarn run format`) --- packages/@uppy/provider-views/src/utils/getClickedRange.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/@uppy/provider-views/src/utils/getClickedRange.ts b/packages/@uppy/provider-views/src/utils/getClickedRange.ts index 660b5b6054..7279f85735 100644 --- a/packages/@uppy/provider-views/src/utils/getClickedRange.ts +++ b/packages/@uppy/provider-views/src/utils/getClickedRange.ts @@ -22,9 +22,9 @@ const getClickedRange = ( const clickedRange = displayedPartialTree.slice( Math.min(lastCheckboxIndex, newCheckboxIndex), Math.max(lastCheckboxIndex, newCheckboxIndex) + 1, - ).map((item) => item.id) + ) - return clickedRange + return clickedRange.map((item) => item.id) } return [clickedId] From 419e2e2e0f0ff5df4a5b6c4e6b8185f50a2bfc20 Mon Sep 17 00:00:00 2001 From: Evgenia Karunus Date: Thu, 13 Jun 2024 19:29:02 +0400 Subject: [PATCH 156/170] `clone.ts` - rename to `shallowClone.ts` --- .../src/utils/PartialTreeUtils/afterFill.ts | 4 ++-- .../src/utils/PartialTreeUtils/afterToggleCheckbox.ts | 4 ++-- .../src/utils/PartialTreeUtils/clone.ts | 9 --------- .../src/utils/PartialTreeUtils/shallowClone.ts | 11 +++++++++++ 4 files changed, 15 insertions(+), 13 deletions(-) delete mode 100644 packages/@uppy/provider-views/src/utils/PartialTreeUtils/clone.ts create mode 100644 packages/@uppy/provider-views/src/utils/PartialTreeUtils/shallowClone.ts diff --git a/packages/@uppy/provider-views/src/utils/PartialTreeUtils/afterFill.ts b/packages/@uppy/provider-views/src/utils/PartialTreeUtils/afterFill.ts index 4e1ef142dd..72d9f754a1 100644 --- a/packages/@uppy/provider-views/src/utils/PartialTreeUtils/afterFill.ts +++ b/packages/@uppy/provider-views/src/utils/PartialTreeUtils/afterFill.ts @@ -7,7 +7,7 @@ import type { } from '@uppy/core/lib/Uppy' import type { CompanionFile } from '@uppy/utils/lib/CompanionFile' import PQueue from 'p-queue' -import clone from './clone.ts' +import shallowClone from './shallowClone.ts' export interface ApiList { (directory: PartialTreeId): Promise<{ @@ -79,7 +79,7 @@ const afterFill = async ( const queue = new PQueue({ concurrency: 6 }) // fill up the missing parts of a partialTree! - const poorTree: PartialTree = clone(partialTree) + const poorTree: PartialTree = shallowClone(partialTree) const poorFolders = poorTree.filter( (item) => item.type === 'folder' && diff --git a/packages/@uppy/provider-views/src/utils/PartialTreeUtils/afterToggleCheckbox.ts b/packages/@uppy/provider-views/src/utils/PartialTreeUtils/afterToggleCheckbox.ts index bc4b6ccd8e..911f7baab2 100644 --- a/packages/@uppy/provider-views/src/utils/PartialTreeUtils/afterToggleCheckbox.ts +++ b/packages/@uppy/provider-views/src/utils/PartialTreeUtils/afterToggleCheckbox.ts @@ -6,7 +6,7 @@ import type { PartialTreeFolderNode, PartialTreeId, } from '@uppy/core/lib/Uppy' -import clone from './clone.ts' +import shallowClone from './shallowClone.ts' /* FROM | TO @@ -85,7 +85,7 @@ const afterToggleCheckbox = ( oldTree: PartialTree, clickedRange: string[], ): PartialTree => { - const tree: PartialTree = clone(oldTree) + const tree: PartialTree = shallowClone(oldTree) if (clickedRange.length >= 2) { // We checked two or more items diff --git a/packages/@uppy/provider-views/src/utils/PartialTreeUtils/clone.ts b/packages/@uppy/provider-views/src/utils/PartialTreeUtils/clone.ts deleted file mode 100644 index 14f78ab7fb..0000000000 --- a/packages/@uppy/provider-views/src/utils/PartialTreeUtils/clone.ts +++ /dev/null @@ -1,9 +0,0 @@ -import type { PartialTree } from '@uppy/core/lib/Uppy' - -// One-level copying is enough, because we're never mutating `.data = { THIS }` within our `partialTree` - -// we're only ever mutating stuff like `.status`, `.cached`, `.nextPagePath`. -const clone = (partialTree: PartialTree): PartialTree => { - return partialTree.map((item) => ({ ...item })) -} - -export default clone diff --git a/packages/@uppy/provider-views/src/utils/PartialTreeUtils/shallowClone.ts b/packages/@uppy/provider-views/src/utils/PartialTreeUtils/shallowClone.ts new file mode 100644 index 0000000000..c81cda89f3 --- /dev/null +++ b/packages/@uppy/provider-views/src/utils/PartialTreeUtils/shallowClone.ts @@ -0,0 +1,11 @@ +import type { PartialTree } from '@uppy/core/lib/Uppy' + +/** + * One-level copying is sufficient as mutations within our `partialTree` are limited to properties + * such as `.status`, `.cached`, `.nextPagePath`, and not `.data = { THIS }`. + */ +const shallowClone = (partialTree: PartialTree): PartialTree => { + return partialTree.map((item) => ({ ...item })) +} + +export default shallowClone From dae378c83caafe7b948641dc6aac2f0abf061c53 Mon Sep 17 00:00:00 2001 From: Evgenia Karunus Date: Thu, 13 Jun 2024 19:39:53 +0400 Subject: [PATCH 157/170] Antoine: in `package.json`, move `devDependencies` up --- packages/@uppy/provider-views/package.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/@uppy/provider-views/package.json b/packages/@uppy/provider-views/package.json index 98b5c68035..6990cd064a 100644 --- a/packages/@uppy/provider-views/package.json +++ b/packages/@uppy/provider-views/package.json @@ -24,11 +24,11 @@ "p-queue": "^8.0.0", "preact": "^10.5.13" }, + "devDependencies": { + "vitest": "^1.6.0" + }, "peerDependencies": { "@uppy/core": "workspace:^" }, - "stableVersion": "3.11.0", - "devDependencies": { - "vitest": "^1.6.0" - } + "stableVersion": "3.11.0" } From 1181a46fb3a4fec1727de4f23f221420d25c7d0f Mon Sep 17 00:00:00 2001 From: Evgenia Karunus Date: Thu, 13 Jun 2024 19:45:24 +0400 Subject: [PATCH 158/170] Antoine: rename `getNOfSelectedFiles()` to `getNumberOfSelectedFiles()` --- packages/@uppy/provider-views/src/FooterActions.tsx | 4 ++-- ...getNOfSelectedFiles.ts => getNumberOfSelectedFiles.ts} | 4 ++-- .../src/utils/PartialTreeUtils/index.test.ts | 8 ++++---- 3 files changed, 8 insertions(+), 8 deletions(-) rename packages/@uppy/provider-views/src/utils/PartialTreeUtils/{getNOfSelectedFiles.ts => getNumberOfSelectedFiles.ts} (86%) diff --git a/packages/@uppy/provider-views/src/FooterActions.tsx b/packages/@uppy/provider-views/src/FooterActions.tsx index b43cd32632..b297d5424f 100644 --- a/packages/@uppy/provider-views/src/FooterActions.tsx +++ b/packages/@uppy/provider-views/src/FooterActions.tsx @@ -4,7 +4,7 @@ import type { Meta, Body } from '@uppy/utils/lib/UppyFile' import classNames from 'classnames' import type { PartialTree } from '@uppy/core/lib/Uppy' import { useMemo } from 'preact/hooks' -import getNOfSelectedFiles from './utils/PartialTreeUtils/getNOfSelectedFiles.ts' +import getNumberOfSelectedFiles from './utils/PartialTreeUtils/getNumberOfSelectedFiles.ts' import ProviderView from './ProviderView/ProviderView.tsx' export default function FooterActions({ @@ -28,7 +28,7 @@ export default function FooterActions({ }, [partialTree, validateAggregateRestrictions]) const nOfSelectedFiles = useMemo(() => { - return getNOfSelectedFiles(partialTree) + return getNumberOfSelectedFiles(partialTree) }, [partialTree]) if (nOfSelectedFiles === 0) { diff --git a/packages/@uppy/provider-views/src/utils/PartialTreeUtils/getNOfSelectedFiles.ts b/packages/@uppy/provider-views/src/utils/PartialTreeUtils/getNumberOfSelectedFiles.ts similarity index 86% rename from packages/@uppy/provider-views/src/utils/PartialTreeUtils/getNOfSelectedFiles.ts rename to packages/@uppy/provider-views/src/utils/PartialTreeUtils/getNumberOfSelectedFiles.ts index 40512f5a68..a8169f8a4d 100644 --- a/packages/@uppy/provider-views/src/utils/PartialTreeUtils/getNOfSelectedFiles.ts +++ b/packages/@uppy/provider-views/src/utils/PartialTreeUtils/getNumberOfSelectedFiles.ts @@ -3,7 +3,7 @@ import type { PartialTree } from '@uppy/core/lib/Uppy' // We're interested in all 'checked' leaves of this tree - // I believe it's the most intuitive number we can show to the user // given we don't have full information about how many files are inside of each selected folder. -const getNOfSelectedFiles = (partialTree: PartialTree): number => { +const getNumberOfSelectedFiles = (partialTree: PartialTree): number => { const checkedLeaves = partialTree.filter((item) => { if (item.type === 'file' && item.status === 'checked') { return true @@ -19,4 +19,4 @@ const getNOfSelectedFiles = (partialTree: PartialTree): number => { return checkedLeaves.length } -export default getNOfSelectedFiles +export default getNumberOfSelectedFiles diff --git a/packages/@uppy/provider-views/src/utils/PartialTreeUtils/index.test.ts b/packages/@uppy/provider-views/src/utils/PartialTreeUtils/index.test.ts index 6cf741df12..0d1b968f7e 100644 --- a/packages/@uppy/provider-views/src/utils/PartialTreeUtils/index.test.ts +++ b/packages/@uppy/provider-views/src/utils/PartialTreeUtils/index.test.ts @@ -13,7 +13,7 @@ import afterOpenFolder from './afterOpenFolder.ts' import afterScrollFolder from './afterScrollFolder.ts' import afterFill from './afterFill.ts' import getCheckedFilesWithPaths from './getCheckedFilesWithPaths.ts' -import getNOfSelectedFiles from './getNOfSelectedFiles.ts' +import getNumberOfSelectedFiles from './getNumberOfSelectedFiles.ts' import getBreadcrumbs from './getBreadcrumbs.ts' const _root = (id: string, options: any = {}): PartialTreeFolderRoot => ({ @@ -472,7 +472,7 @@ describe('afterToggleCheckbox()', () => { }) }) -describe('getNOfSelectedFiles()', () => { +describe('getNumberOfSelectedFiles()', () => { it('gets all leaf items', () => { // prettier-ignore const tree: PartialTree = [ @@ -486,7 +486,7 @@ describe('getNOfSelectedFiles()', () => { // leaf .checked file _file('2_2', { parentId: '2', status: 'checked' }), ] - const result = getNOfSelectedFiles(tree) + const result = getNumberOfSelectedFiles(tree) expect(result).toEqual(3) }) @@ -497,7 +497,7 @@ describe('getNOfSelectedFiles()', () => { // empty .checked .cached folder _folder('1', { parentId: 'ourRoot', cached: true, status: 'checked' }), ] - const result = getNOfSelectedFiles(tree) + const result = getNumberOfSelectedFiles(tree) // This should be "1" for more pleasant UI - if the user unchecks this folder, // they should immediately see "Selected (1)" turning into "Selected (0)". expect(result).toEqual(1) From 495c92a8e3d4b37158f78c44804bdcbd5acd6bfe Mon Sep 17 00:00:00 2001 From: Evgenia Karunus Date: Thu, 13 Jun 2024 19:52:22 +0400 Subject: [PATCH 159/170] `getNumberOfSelectedFiles()` - better comments --- .../utils/PartialTreeUtils/getNumberOfSelectedFiles.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/@uppy/provider-views/src/utils/PartialTreeUtils/getNumberOfSelectedFiles.ts b/packages/@uppy/provider-views/src/utils/PartialTreeUtils/getNumberOfSelectedFiles.ts index a8169f8a4d..d27a3e18f7 100644 --- a/packages/@uppy/provider-views/src/utils/PartialTreeUtils/getNumberOfSelectedFiles.ts +++ b/packages/@uppy/provider-views/src/utils/PartialTreeUtils/getNumberOfSelectedFiles.ts @@ -1,8 +1,11 @@ import type { PartialTree } from '@uppy/core/lib/Uppy' -// We're interested in all 'checked' leaves of this tree - -// I believe it's the most intuitive number we can show to the user -// given we don't have full information about how many files are inside of each selected folder. +/** + * We're interested in all 'checked' leaves of this tree, + * but we don't yet know how many files there are inside of each checked folder. + * `getNumberOfSelectedFiles()` returns the most intuitive number we can show to the user + * in this situation. + */ const getNumberOfSelectedFiles = (partialTree: PartialTree): number => { const checkedLeaves = partialTree.filter((item) => { if (item.type === 'file' && item.status === 'checked') { From b7e901faaecbda9539b81c1e5301ec3502f0c6a2 Mon Sep 17 00:00:00 2001 From: Evgenia Karunus Date: Thu, 13 Jun 2024 21:31:45 +0400 Subject: [PATCH 160/170] Antoine: remove `
    ` tag --- .../@uppy/provider-views/src/SearchInput.tsx | 22 ++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/packages/@uppy/provider-views/src/SearchInput.tsx b/packages/@uppy/provider-views/src/SearchInput.tsx index ea50a1f7c8..43c8fef42d 100644 --- a/packages/@uppy/provider-views/src/SearchInput.tsx +++ b/packages/@uppy/provider-views/src/SearchInput.tsx @@ -30,8 +30,15 @@ function SearchInput({ showButton = false, buttonLabel = '', }: Props) { - const onSubmit = (e: Event) => { - e.preventDefault() + const onSubmitFromInput = ( + e: h.JSX.TargetedKeyboardEvent, + ) => { + if (e.key === 'Enter' || e.keyCode === 13) { + submitSearchString() + } + } + + const onSubmitFromButton = () => { submitSearchString() } @@ -40,7 +47,10 @@ function SearchInput({ } return ( - + // Notice we intentionally do not use the tag here, + // because we're trying to avoid the "nested tags" issue + // (see github.com/transloadit/uppy/pull/5050#discussion_r1638260456) +
    {!showButton && ( @@ -85,12 +96,13 @@ function SearchInput({ {showButton && ( )} - +
    ) } From 7dccd0e276588a093f3583892809b06f95ece2bb Mon Sep 17 00:00:00 2001 From: Evgenia Karunus Date: Thu, 13 Jun 2024 21:52:16 +0400 Subject: [PATCH 161/170] Antoine: change `{}` to `Object.create(null)`, write tests --- .../PartialTreeUtils/getCheckedFilesWithPaths.ts | 3 ++- .../src/utils/PartialTreeUtils/index.test.ts | 16 +++++++++++++++- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/packages/@uppy/provider-views/src/utils/PartialTreeUtils/getCheckedFilesWithPaths.ts b/packages/@uppy/provider-views/src/utils/PartialTreeUtils/getCheckedFilesWithPaths.ts index 8710b18ff1..a438a654fa 100644 --- a/packages/@uppy/provider-views/src/utils/PartialTreeUtils/getCheckedFilesWithPaths.ts +++ b/packages/@uppy/provider-views/src/utils/PartialTreeUtils/getCheckedFilesWithPaths.ts @@ -33,7 +33,8 @@ const getPath = ( const getCheckedFilesWithPaths = ( partialTree: PartialTree, ): CompanionFile[] => { - const cache: Cache = {} + // Equivalent to `const cache = {}`, but makes keys such as 'hasOwnProperty' safe too + const cache: Cache = Object.create(null) // We're only interested in injecting paths into 'checked' files const checkedFiles = partialTree.filter( diff --git a/packages/@uppy/provider-views/src/utils/PartialTreeUtils/index.test.ts b/packages/@uppy/provider-views/src/utils/PartialTreeUtils/index.test.ts index 0d1b968f7e..d48b961879 100644 --- a/packages/@uppy/provider-views/src/utils/PartialTreeUtils/index.test.ts +++ b/packages/@uppy/provider-views/src/utils/PartialTreeUtils/index.test.ts @@ -504,7 +504,7 @@ describe('getNumberOfSelectedFiles()', () => { }) }) -describe('injectPaths()', () => { +describe('getCheckedFilesWithPaths()', () => { // Note that this is a tree that doesn't require any api calls, everything is cached already // prettier-ignore const tree: PartialTree = [ @@ -548,6 +548,20 @@ describe('injectPaths()', () => { 'name_2_4/name_2_4_1.jpg', ) }) + + // (See github.com/transloadit/uppy/pull/5050#discussion_r1638523560) + it('file ids such as "hasOwnProperty" are safe', () => { + const weirdIdsTree = [ + _root('ourRoot'), + _folder('1', { parentId: 'ourRoot', status: 'checked' }), + _file('hasOwnProperty', { parentId: '1', status: 'checked' }), + ] + const result = getCheckedFilesWithPaths(weirdIdsTree) + + expect(result.find((f) => f.id === 'hasOwnProperty')!.relDirPath).toEqual( + 'name_1/name_hasOwnProperty.jpg', + ) + }) }) describe('getBreadcrumbs()', () => { From dea7e51b865c34a2473102633e0a579ff3f1c2ef Mon Sep 17 00:00:00 2001 From: Evgenia Karunus Date: Sat, 15 Jun 2024 03:11:59 +0400 Subject: [PATCH 162/170] Antoine: `` - return dynamic
    element --- packages/@uppy/provider-views/package.json | 1 + .../@uppy/provider-views/src/SearchInput.tsx | 53 ++++++++++++------- yarn.lock | 1 + 3 files changed, 35 insertions(+), 20 deletions(-) diff --git a/packages/@uppy/provider-views/package.json b/packages/@uppy/provider-views/package.json index 6990cd064a..9af1cb6703 100644 --- a/packages/@uppy/provider-views/package.json +++ b/packages/@uppy/provider-views/package.json @@ -21,6 +21,7 @@ "dependencies": { "@uppy/utils": "workspace:^", "classnames": "^2.2.6", + "nanoid": "^5.0.0", "p-queue": "^8.0.0", "preact": "^10.5.13" }, diff --git a/packages/@uppy/provider-views/src/SearchInput.tsx b/packages/@uppy/provider-views/src/SearchInput.tsx index 43c8fef42d..1fb15e47cf 100644 --- a/packages/@uppy/provider-views/src/SearchInput.tsx +++ b/packages/@uppy/provider-views/src/SearchInput.tsx @@ -1,5 +1,7 @@ import { h } from 'preact' -import type { ChangeEvent } from 'preact/compat' +import { useEffect, useState, useCallback } from 'preact/hooks' +import { type ChangeEvent } from 'preact/compat' +import { nanoid } from 'nanoid/non-secure' type Props = { searchString: string @@ -30,27 +32,38 @@ function SearchInput({ showButton = false, buttonLabel = '', }: Props) { - const onSubmitFromInput = ( - e: h.JSX.TargetedKeyboardEvent, - ) => { - if (e.key === 'Enter' || e.keyCode === 13) { - submitSearchString() - } - } - - const onSubmitFromButton = () => { - submitSearchString() - } - const onInput = (e: ChangeEvent) => { setSearchString((e.target as HTMLInputElement).value) } + const submit = useCallback( + (ev: Event) => { + ev.preventDefault() + submitSearchString() + }, + [submitSearchString], + ) + + // We do this to avoid nested s + // (See https://github.com/transloadit/uppy/pull/5050#discussion_r1640392516) + const [form] = useState(() => { + const formEl = document.createElement('form') + formEl.setAttribute('tabindex', '-1') + formEl.id = nanoid() + return formEl + }) + + useEffect(() => { + document.body.appendChild(form) + form.addEventListener('submit', submit) + return () => { + form.removeEventListener('submit', submit) + document.body.removeChild(form) + } + }, [form, submit]) + return ( - // Notice we intentionally do not use the tag here, - // because we're trying to avoid the "nested tags" issue - // (see github.com/transloadit/uppy/pull/5050#discussion_r1638260456) -
    +
    {!showButton && ( @@ -96,8 +109,8 @@ function SearchInput({ {showButton && ( diff --git a/yarn.lock b/yarn.lock index 149e25391d..7d2c1a5fd0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8879,6 +8879,7 @@ __metadata: dependencies: "@uppy/utils": "workspace:^" classnames: "npm:^2.2.6" + nanoid: "npm:^5.0.0" p-queue: "npm:^8.0.0" preact: "npm:^10.5.13" vitest: "npm:^1.6.0" From 563435a4f3221f56a5346900fb6c8e4f290ee448 Mon Sep 17 00:00:00 2001 From: Evgenia Karunus Date: Sat, 15 Jun 2024 03:24:05 +0400 Subject: [PATCH 163/170] `` - return `buttonCSSClassName` --- packages/@uppy/provider-views/src/SearchInput.tsx | 4 +++- .../src/SearchProviderView/SearchProviderView.tsx | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/@uppy/provider-views/src/SearchInput.tsx b/packages/@uppy/provider-views/src/SearchInput.tsx index 1fb15e47cf..878b6e93fa 100644 --- a/packages/@uppy/provider-views/src/SearchInput.tsx +++ b/packages/@uppy/provider-views/src/SearchInput.tsx @@ -16,6 +16,7 @@ type Props = { showButton?: boolean buttonLabel?: string + buttonCSSClassName?: string } function SearchInput({ @@ -31,6 +32,7 @@ function SearchInput({ showButton = false, buttonLabel = '', + buttonCSSClassName = '', }: Props) { const onInput = (e: ChangeEvent) => { setSearchString((e.target as HTMLInputElement).value) @@ -108,7 +110,7 @@ function SearchInput({ )} {showButton && ( // label for a checkbox diff --git a/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx b/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx index 821ef105a5..021bc797d2 100644 --- a/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx +++ b/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx @@ -83,7 +83,12 @@ export interface Opts { } type PassedOpts = Optional< Opts, - 'viewType' | 'showTitles' | 'showFilter' | 'showBreadcrumbs' | 'loadAllFiles' | 'virtualList' + | 'viewType' + | 'showTitles' + | 'showFilter' + | 'showBreadcrumbs' + | 'loadAllFiles' + | 'virtualList' > type DefaultOpts = Omit, 'provider'> type RenderOpts = Omit< @@ -117,7 +122,7 @@ export default class ProviderView { showFilter: true, showBreadcrumbs: true, loadAllFiles: false, - virtualList: false + virtualList: false, } this.opts = { ...defaultOptions, ...opts } From bb0b2b8431f5a8dbb4d9d3408ee3ad56518c1f71 Mon Sep 17 00:00:00 2001 From: Evgenia Karunus Date: Thu, 20 Jun 2024 05:06:13 +0400 Subject: [PATCH 168/170] `Facebook.tsx`, `GooglePhotos.tsx` - render in 'grid' style on per-folder basis --- packages/@uppy/facebook/src/Facebook.tsx | 9 ++++++--- packages/@uppy/google-photos/src/GooglePhotos.tsx | 9 ++++++--- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/packages/@uppy/facebook/src/Facebook.tsx b/packages/@uppy/facebook/src/Facebook.tsx index 4219e121cb..9345ebff9a 100644 --- a/packages/@uppy/facebook/src/Facebook.tsx +++ b/packages/@uppy/facebook/src/Facebook.tsx @@ -103,10 +103,13 @@ export default class Facebook extends UIPlugin< } render(state: unknown): ComponentChild { - const { partialTree } = this.getPluginState() - const folders = partialTree.filter((i) => i.type === 'folder') + const { partialTree, currentFolderId } = this.getPluginState() - if (folders.length === 0) { + const foldersInThisFolder = partialTree.filter( + (i) => i.type === 'folder' && i.parentId === currentFolderId, + ) + + if (foldersInThisFolder.length === 0) { return this.view.render(state, { viewType: 'grid', showFilter: false, diff --git a/packages/@uppy/google-photos/src/GooglePhotos.tsx b/packages/@uppy/google-photos/src/GooglePhotos.tsx index 52490baa06..befa69eab3 100644 --- a/packages/@uppy/google-photos/src/GooglePhotos.tsx +++ b/packages/@uppy/google-photos/src/GooglePhotos.tsx @@ -114,10 +114,13 @@ export default class GooglePhotos< } render(state: unknown): ComponentChild { - const { partialTree } = this.getPluginState() - const folders = partialTree.filter((i) => i.type === 'folder') + const { partialTree, currentFolderId } = this.getPluginState() - if (folders.length === 0) { + const foldersInThisFolder = partialTree.filter( + (i) => i.type === 'folder' && i.parentId === currentFolderId, + ) + + if (foldersInThisFolder.length === 0) { return this.view.render(state, { viewType: 'grid', showFilter: false, From fc7c7778f6524abecf1b0e7397933ed6a1f1453f Mon Sep 17 00:00:00 2001 From: Evgenia Karunus Date: Thu, 20 Jun 2024 05:16:14 +0400 Subject: [PATCH 169/170] `` - use the `.thumbnail` whenever possible (improves image quality, adds video icons) --- packages/@uppy/provider-views/src/Item/components/GridItem.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@uppy/provider-views/src/Item/components/GridItem.tsx b/packages/@uppy/provider-views/src/Item/components/GridItem.tsx index 4c28e463c1..9dd3cfb1d1 100644 --- a/packages/@uppy/provider-views/src/Item/components/GridItem.tsx +++ b/packages/@uppy/provider-views/src/Item/components/GridItem.tsx @@ -44,7 +44,7 @@ function GridItem({ aria-label={file.data.name} className="uppy-u-reset uppy-ProviderBrowserItem-inner" > - + {showTitles && file.data.name} {children} From 8655bf5e74d2b8deb2e986c978a0baab92e019a9 Mon Sep 17 00:00:00 2001 From: Evgenia Karunus Date: Thu, 20 Jun 2024 05:23:40 +0400 Subject: [PATCH 170/170] `prettier` - ensure `PartialTree` is always strongly indented in tests --- .../src/utils/PartialTreeUtils/index.test.ts | 58 ++++++++++--------- 1 file changed, 31 insertions(+), 27 deletions(-) diff --git a/packages/@uppy/provider-views/src/utils/PartialTreeUtils/index.test.ts b/packages/@uppy/provider-views/src/utils/PartialTreeUtils/index.test.ts index d48b961879..6eacea2ba3 100644 --- a/packages/@uppy/provider-views/src/utils/PartialTreeUtils/index.test.ts +++ b/packages/@uppy/provider-views/src/utils/PartialTreeUtils/index.test.ts @@ -92,10 +92,11 @@ describe('afterFill()', () => { }) it('fetches a .checked folder', async () => { + // prettier-ignore const tree: PartialTree = [ _root('ourRoot'), - _folder('1', { parentId: 'ourRoot' }), - _folder('2', { parentId: 'ourRoot', cached: false, status: 'checked' }), + _folder('1', { parentId: 'ourRoot' }), + _folder('2', { parentId: 'ourRoot', cached: false, status: 'checked' }), ] const mock = (path: PartialTreeId) => { if (path === '2') { @@ -118,15 +119,16 @@ describe('afterFill()', () => { }) it('fetches remaining pages in a folder', async () => { + // prettier-ignore const tree: PartialTree = [ _root('ourRoot'), - _folder('1', { parentId: 'ourRoot' }), - _folder('2', { - parentId: 'ourRoot', - cached: true, - nextPagePath: '666', - status: 'checked', - }), + _folder('1', { parentId: 'ourRoot' }), + _folder('2', { + parentId: 'ourRoot', + cached: true, + nextPagePath: '666', + status: 'checked', + }), ] const mock = (path: PartialTreeId) => { if (path === '666') { @@ -185,24 +187,25 @@ describe('afterFill()', () => { }) it('complex situation', async () => { + // prettier-ignore const tree: PartialTree = [ _root('ourRoot'), - _folder('1', { parentId: 'ourRoot' }), - // folder we'll be recursively fetching really deeply - _folder('2', { - parentId: 'ourRoot', - cached: true, - nextPagePath: '2_next', - status: 'checked', - }), - _file('2_1', { parentId: '2', status: 'checked' }), - _file('2_2', { parentId: '2', status: 'checked' }), - // folder with only some files checked - _folder('3', { parentId: 'ourRoot', cached: true, status: 'partial' }), - // empty folder - _folder('0', { parentId: '3', cached: false, status: 'checked' }), - _file('3_1', { parentId: '3', status: 'checked' }), - _file('3_2', { parentId: '3', status: 'unchecked' }), + _folder('1', { parentId: 'ourRoot' }), + // folder we'll be recursively fetching really deeply + _folder('2', { + parentId: 'ourRoot', + cached: true, + nextPagePath: '2_next', + status: 'checked', + }), + _file('2_1', { parentId: '2', status: 'checked' }), + _file('2_2', { parentId: '2', status: 'checked' }), + // folder with only some files checked + _folder('3', { parentId: 'ourRoot', cached: true, status: 'partial' }), + // empty folder + _folder('0', { parentId: '3', cached: false, status: 'checked' }), + _file('3_1', { parentId: '3', status: 'checked' }), + _file('3_2', { parentId: '3', status: 'unchecked' }), ] const mock = (path: PartialTreeId) => { if (path === '2_next') { @@ -492,10 +495,11 @@ describe('getNumberOfSelectedFiles()', () => { }) it('empty folder, even after being opened, counts as leaf node', () => { + // prettier-ignore const tree: PartialTree = [ _root('ourRoot'), - // empty .checked .cached folder - _folder('1', { parentId: 'ourRoot', cached: true, status: 'checked' }), + // empty .checked .cached folder + _folder('1', { parentId: 'ourRoot', cached: true, status: 'checked' }), ] const result = getNumberOfSelectedFiles(tree) // This should be "1" for more pleasant UI - if the user unchecks this folder,