Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Provider views rewrite (.files, .folders => .partialTree) #5050

Draft
wants to merge 131 commits into
base: 4.x
Choose a base branch
from

Conversation

lakesare
Copy link
Contributor

@lakesare lakesare commented Mar 29, 2024

Description

TODO

  • To Evgenia - don't forget to remove loadAllFiles: false & limit: 5 from providers when preparing for a review
  • To Evgenia - don't forget to remove console.logs

Notes to reviewers

  • I made deliberate effort not to touch the folder structure at all (for ease of reviewing & because we didn't set our minds on which one we'd prefer yet)
  • This PR actually reduces the number of lines by a few hundred lines - the increase is due to the test file I added

GoogleDrive
- travelling down into folders works
- checking a file works
- breadcrumbs DONT work
@Murderlon
Copy link
Member

Won't we eliminate a whole set of problems and a lot of code complexity by simply not allowing folders to be checked? Instead we offer a "select all" checkbox which checks as many as possible within the current folder up until the limit with the current sorting?

I feel like we starting to over-engineer this, causing many different UI states which are more confusing to the user to figure out than the likelihood of them wanting to select entire folders. Ideally we design for the 80% use case, and I think that's selecting individual files.

@lakesare
Copy link
Contributor Author

lakesare commented May 8, 2024

@Murderlon, disagreed, people clearly use folder selection, see all the discussions about relativePath vs absolutePath.
And if we disable this, we'll make downloading large number of files outright impossible.

Also - I regularly use folder selection in the wild myself, for example when you're uploading your assignment on university websites, they expect a particular folder structure to be kept.

@mifi
Copy link
Contributor

mifi commented May 8, 2024

Sounds like it's getting a bit complicated now. Maybe for simplicity of implementation we could:

  • Allow the user to check however many files/folder they want without any restrictions in the UI
  • Once the user clicks "select (X) files", then we recurse through all selected directories and count all files. Once we exceed the limit, the we do NOT add any files, but instead show an error message "Uppy only allows X files, but Y files were selected". then the user can go back and deselect files/folders.

@Murderlon
Copy link
Member

First want to say that thanks for putting in big effort of cleaning this up AND writing a lot tests ✨


If we want to keep folder selection, I would follow these options

Situation 1: max files set to 3, you select a folder & then open it, it has 4 files.
--> It shows those 4 files as selected, but, when we click SELECT (4), we get some error notification

Situation 2: identical to situation one from my understanding.

Situation 3: I click "Select (2)". There are 1000 files inside of those folders, which violates our restrictions.
--> I think once again the same? If you open them one by one, you'll see "select {number}" but once you click it none are added and you see an error

Situation 5: should checkboxes be disabled once you reach the limit.
--> I don't think we have to to simplify it + we kind of have to, as we allow selecting beyond the limit in the other examples.


I do think it's essential however to break this PR up into smaller PRs. I understand this made sense to experiment and find a direction, but once we have that I expect this to be four PRs or so. For instance:

  1. shift click fixes
  2. removing the View class
  3. Adding basic partialTree structure
  4. refactor of ProviderView/SearchProviderView.

It will be too hard to review otherwise I'm afraid.

@lakesare
Copy link
Contributor Author

lakesare commented May 9, 2024

@mifi Sounds like it's getting a bit complicated now. Maybe for simplicity of implementation we could:

  1. Allow the user to check however many files/folder they want without any restrictions in the UI
  2. Once the user clicks "select (X) files", then we recurse through all selected directories and count all files. Once we exceed the limit, the we do NOT add any files, but instead show an error message "Uppy only allows X files, but Y files were selected". then the user can go back and deselect files/folders.

I like this idea, however consider the following situation.
Currently, when we have const restrictions = { maxNumberOfFiles: 1 } and we check a single file, all the other files become unselectable:

image

If we implement "restriction validation only on Select (x)", the user will have no way to discover whether they have selected too many files - imagine having selected 10 files only to discover 3 files were allowed. This is especially important for the maxTotalFileSize restriction, because file sizes are harder to predict. Also - I believe const restrictions = { maxNumberOfFiles: 1 } is a pretty frequent use case, and all the files greying out upon the selection of a single file is attractive.

I agree it's the simplest implementation possible, in fact what you're suggesting is exactly what I decided to do a week or so ago in order to "ship this PR, and maybe think through better restrictions system in further PRs".

But I think the situation I just described is a serious downside, so I decided to ask for Alex's help in coming up with the "ideal restrictions system".
Once we know what an "ideal restrictions system" would look like, it will be easier to see what I must implement in this PR, and what we can leave to others.


One variant of what you're suggesting is "allow the user to check however many files/folders they want without any restrictions in the UI; but show the error in the footer next to Select (x) if they violate some restrictions".

This solves the issue I just described; but does remove the attractive greying-out of the checkboxes (we will only show the error when the user checks 2 files, we won't see any feedback when the user checks 1 file).

@nqst
Copy link
Contributor

nqst commented May 9, 2024

I agree we should keep it simple. I don't think we should be engineering an "ideal" system right now. We could think about some magic solution(s), but it seems it's going to be prone to bugs and always have edge cases which does more harm than good both for us and the users.

So, I mostly agree with Merlijn's suggested solutions and Mikael's idea to:

Allow the user to check however many files/folder they want without any restrictions in the UI

It's not a significant issue to allow users to select files and then show an error if restrictions aren't respected. However, I have some ideas that can hopefully make the entire experience smoother:

  1. We could display the restriction message when the user is selecting files as well. The initial message about restrictions on the home screen can be easily ignored, but having the message visible in the file browser may help prevent incorrect actions.

  2. I like how nicely we currently support the case when the user can select only one file, and I agree that would be good to keep this. My idea is to keep this behavior only for { maxNumberOfFiles: 1 }, similar to a regular <input type="file"> without the multiple attribute set. Therefore, if the widely-used single-file mode is on, we'll disable the rest of the files when one file is selected. But if a more advanced restriction mode is enabled, let's not over-engineer it — allow users to select what they want, and then show an error if something isn't right.

  3. The error message should be more clear. When testing, I saw this:

    It doesn't look good to me. The messages contradict each other, and the error doesn't look like an error. It would be great to improve this.

What do you folks think?

@mifi
Copy link
Contributor

mifi commented May 9, 2024

One variant of what you're suggesting is "allow the user to check however many files/folders they want without any restrictions in the UI; but show the error in the footer next to Select (x) if they violate some restrictions".

This solves the issue I just described; but does remove the attractive greying-out of the checkboxes (we will only show the error when the user checks 2 files, we won't see any feedback when the user checks 1 file).

I agree. Maybe we could even grey out checkboxes if the user has reached the limit. It will not be 100% correct (because the user might have selected at least one folder which contains more files), however it's an optimistic guess. The only problem I see (with greying out optimistically based on number of checked checkboxes) is that if the user checks a folder, but later it turns out the folder is empty, then the folder was counted as 1 file towards the limit, but in reality there are no files inside, so the user could actually have added 1 more file. Or maybe we should count checked folders as 0 towards the limit.

@lakesare
Copy link
Contributor Author

lakesare commented May 10, 2024

@nqst,

  1. Making this work for { maxNumberOfFiles: 1 } is not significantly easier than making it work for { maxNumberOfFiles: 200 }. We get into the same "5 situations" I described above.
  2. Agreed error messages have to be reworked.

@lakesare
Copy link
Contributor Author

lakesare commented May 10, 2024

@nqst: I agree we should keep it simple. I don't think we should be engineering an "ideal" system right now. We could think about some magic solution(s), but it seems it's going to be prone to bugs and always have edge cases which does more harm than good both for us and the users.

"Ideal" system doesn't necessarily bring complexity in implementation with it!
We don't have to think it through now however, I'm friendly towards the idea of implementing a bare minimum solution for providers in this PR, and drawing upon this blank slate later, after we decide on a better restrictions system.

Let's think about what would be the minimal restrictions system in this PR that we are willing to accept.
The easiest thing to implement would indeed be @Murderlon and @mifi -suggested "we pretend there are no aggregate restrictions" solution.

I will describe to you 2 solutions that are equally easy to implement.
In both of these solutions we are not greying out files (to be more precise - we are greying out files with individual restrictions, but not files with aggregate restrictions).

Solution 1: error on click

const restrictions = { maxNumberOfFiles: 3 }
  1. User checks as many files as they like, there is no feedback telling them they are doing something wrong. User checks 5 files.
  2. User clicks "Select (X)"
  3. User sees the notification "Please select at most 3 files"
  4. User is still in the GoogleDrive interface - they are free to uncheck some files and try clicking "Select (X)" again.

Solution 2: error in the footer

const restrictions = { maxNumberOfFiles: 3 }
  1. User checks as many files as they like. User checks 5 files.

  2. In the footer, they see the "You can only select 3 files" error. The "Select (X)" button is disabled.

    image
  3. User unchecks 2 files. Error disappears.

    image

@nqst, @mifi, @Murderlon - do you find either of these solutions acceptable, and do you have a preference for one over the other?

@mifi
Copy link
Contributor

mifi commented May 10, 2024

if we are not greying out checkboxes once the limit has been reached, then i like solution 2 more

@Murderlon
Copy link
Member

Option 2 sounds good!

@nqst
Copy link
Contributor

nqst commented May 13, 2024

@lakesare I also like the second solution you proposed 👍

Making this work for { maxNumberOfFiles: 1 } is not significantly easier than making it work for { maxNumberOfFiles: 200 }. We get into the same "5 situations" I described above.

I think in the case of { maxNumberOfFiles: 1 }, we could apply Merlijn's idea to remove the folder selection. This change would immediately resolve situations 1-4, and make the UI clearer. As for situation 5, we can gray out remaining files after one file has been selected.

This means that the one-file-only scenario would have its own distinct behavior, but I believe this could be beneficial and less confusing for users.

What do you think?

@lakesare
Copy link
Contributor Author

lakesare commented May 13, 2024

@nqst,
I think if we have the "error in the footer", we don't need to disable folders to accompany the greying-out; "error in the footer" already deals with the 5 situations I described.

So, now we have the following choice.

Solution 1

  • show the error in the footer
  • DON'T grey out anything

Solution 2

  • show the error in the footer
  • additionally grey out files if we have { maxNumberOfFiles: 1 }

Solution 3

  • show the error in the footer
  • additionally grey out files if we have { maxNumberOfFiles: any } or { maxTotalFileSize: any }

Thing is - Solution 2 and Solution 3 are about equal in difficulty/overengineering, for both we need custom handling.
So, I would either go for Solution 1, or for Solution 3.

I think Solution 3 is already close to one variant of what I described as "ideal restriction systems".
But I feel we are locking ourselves into this option instead of thinking through the alternative "ideal restriction systems". One alternative would be what I think I described to you in our call - showing users the error in the footer; but letting them proceed with the selection, so that they can remove excessive files in the upload view.

I think that unless you think Solution 3 is a good option long-term, I should just implement Solution 1 in this PR, and leave further thinking on this topic to other PRs.

@nqst
Copy link
Contributor

nqst commented May 16, 2024

Solution 1 (show the error in the footer + don't grey out anything) sounds like a good choice to implement in this PR — makes a lot of sense 👌 When it's done, tho, I propose testing the one-file scenario and exploring nicer UI possibilities for this case in the future.

@lakesare
Copy link
Contributor Author

When it's done, tho, I propose testing the one-file scenario and exploring nicer UI possibilities for this case in the future.

Agreed.
I do like the idea of disabling folder selection for { maxNumberOfFiles: 1 } as an additional prettification of ui, let's think that through in further PRs.

Thank you all for your input @nqst, @Murderlon, @mifi!
I think we settled on a solid choice, I will invite you all for a review when the PR is ready 👍

@lakesare lakesare changed the base branch from main to 4.x May 31, 2024 05:59
Copy link
Contributor

Diff output files
diff --git a/packages/@uppy/companion/lib/server/provider/drive/index.js b/packages/@uppy/companion/lib/server/provider/drive/index.js
index c5381fd..db8719e 100644
--- a/packages/@uppy/companion/lib/server/provider/drive/index.js
+++ b/packages/@uppy/companion/lib/server/provider/drive/index.js
@@ -95,7 +95,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: 10,
           orderBy: "folder,name",
           includeItemsFromAllDrives: true,
           supportsAllDrives: true,
diff --git a/packages/@uppy/companion/lib/server/provider/instagram/graph/index.js b/packages/@uppy/companion/lib/server/provider/instagram/graph/index.js
index c674bbe..be5fa92 100644
--- a/packages/@uppy/companion/lib/server/provider/instagram/graph/index.js
+++ b/packages/@uppy/companion/lib/server/provider/instagram/graph/index.js
@@ -41,6 +41,7 @@ class Instagram extends Provider {
       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/lib/Restricter.js b/packages/@uppy/core/lib/Restricter.js
index 7a5ac8b..ca9741c 100644
--- a/packages/@uppy/core/lib/Restricter.js
+++ b/packages/@uppy/core/lib/Restricter.js
@@ -53,22 +53,15 @@ class Restricter {
       }
     }
     if (maxTotalFileSize) {
-      let totalFilesSize = existingFiles.reduce((total, f) => {
+      let totalFilesSize = [...existingFiles, ...addingFiles].reduce((total, f) => {
         var _f$size;
         return total + ((_f$size = f.size) != null ? _f$size : 0);
       }, 0);
-      for (const addingFile of addingFiles) {
-        if (addingFile.size != null) {
-          totalFilesSize += addingFile.size;
-          if (totalFilesSize > maxTotalFileSize) {
-            throw new RestrictionError(
-              this.getI18n()("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/lib/Uppy.js b/packages/@uppy/core/lib/Uppy.js
index 8cd1571..a9c0089 100644
--- a/packages/@uppy/core/lib/Uppy.js
+++ b/packages/@uppy/core/lib/Uppy.js
@@ -473,14 +473,20 @@ export class Uppy {
       isSomeGhost: files.some(file => file.isGhost),
     };
   }
-  validateRestrictions(file, files) {
-    if (files === void 0) {
-      files = this.getFiles();
+  validateSingleFile(file) {
+    try {
+      _classPrivateFieldLooseBase(this, _restricter)[_restricter].validateSingleFile(file);
+    } catch (err) {
+      return err.message;
     }
+    return null;
+  }
+  validateAggregateRestrictions(files) {
+    const existingFiles = this.getFiles();
     try {
-      _classPrivateFieldLooseBase(this, _restricter)[_restricter].validate(files, [file]);
+      _classPrivateFieldLooseBase(this, _restricter)[_restricter].validateAggregateRestrictions(existingFiles, files);
     } catch (err) {
-      return err;
+      return err.message;
     }
     return null;
   }
diff --git a/packages/@uppy/core/lib/locale.js b/packages/@uppy/core/lib/locale.js
index d0707fb..cdb4f7c 100644
--- a/packages/@uppy/core/lib/locale.js
+++ b/packages/@uppy/core/lib/locale.js
@@ -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: "Missing required meta fields in %{fileName}",
diff --git a/packages/@uppy/facebook/lib/Facebook.js b/packages/@uppy/facebook/lib/Facebook.js
index 4515abf..c3b7ed5 100644
--- a/packages/@uppy/facebook/lib/Facebook.js
+++ b/packages/@uppy/facebook/lib/Facebook.js
@@ -71,13 +71,19 @@ export default class Facebook extends UIPlugin {
     this.unmount();
   }
   render(state) {
-    const viewOptions = {};
-    if (this.getPluginState().files.length && !this.getPluginState().folders.length) {
-      viewOptions.viewType = "grid";
-      viewOptions.showFilter = false;
-      viewOptions.showTitles = false;
+    const {
+      partialTree,
+    } = this.getPluginState();
+    const folders = partialTree.filter(i => i.type === "folder");
+    if (folders.length === 0) {
+      return this.view.render(state, {
+        viewType: "grid",
+        showFilter: false,
+        showTitles: false,
+      });
+    } else {
+      return this.view.render(state);
     }
-    return this.view.render(state, viewOptions);
   }
 }
 Facebook.VERSION = packageJson.version;
diff --git a/packages/@uppy/google-drive/lib/GoogleDrive.js b/packages/@uppy/google-drive/lib/GoogleDrive.js
index c8ad8f3..9dfd11e 100644
--- a/packages/@uppy/google-drive/lib/GoogleDrive.js
+++ b/packages/@uppy/google-drive/lib/GoogleDrive.js
@@ -1,7 +1,7 @@
 import { getAllowedHosts, Provider, tokenStorage } from "@uppy/companion-client";
 import { UIPlugin } from "@uppy/core";
+import { ProviderViews } from "@uppy/provider-views";
 import { h } from "preact";
-import DriveProviderViews from "./DriveProviderViews.js";
 import locale from "./locale.js";
 const packageJson = {
   "version": "4.0.0-beta.5",
@@ -56,6 +56,7 @@ export default class GoogleDrive extends UIPlugin {
           }),
         ),
       );
+    this.rootFolderId = "root";
     this.opts.companionAllowedHosts = getAllowedHosts(this.opts.companionAllowedHosts, this.opts.companionUrl);
     this.provider = new Provider(uppy, {
       companionUrl: this.opts.companionUrl,
@@ -72,9 +73,9 @@ export default class GoogleDrive extends UIPlugin {
     this.render = this.render.bind(this);
   }
   install() {
-    this.view = new DriveProviderViews(this, {
+    this.view = new ProviderViews(this, {
       provider: this.provider,
-      loadAllFiles: true,
+      loadAllFiles: false,
     });
     const {
       target,
diff --git a/packages/@uppy/instagram/lib/Instagram.js b/packages/@uppy/instagram/lib/Instagram.js
index 57e1667..de2153b 100644
--- a/packages/@uppy/instagram/lib/Instagram.js
+++ b/packages/@uppy/instagram/lib/Instagram.js
@@ -59,6 +59,7 @@ export default class Instagram extends UIPlugin {
           }),
         ),
       );
+    this.rootFolderId = "recent";
     this.defaultLocale = locale;
     this.i18nInit();
     this.title = this.i18n("pluginNameInstagram");
diff --git a/packages/@uppy/provider-views/lib/Breadcrumbs.js b/packages/@uppy/provider-views/lib/Breadcrumbs.js
index 8364b3e..a911a5c 100644
--- a/packages/@uppy/provider-views/lib/Breadcrumbs.js
+++ b/packages/@uppy/provider-views/lib/Breadcrumbs.js
@@ -1,24 +1,7 @@
 import { Fragment, h } from "preact";
-const Breadcrumb = props => {
-  const {
-    getFolder,
-    title,
-    isLast,
-  } = props;
-  return h(
-    Fragment,
-    null,
-    h("button", {
-      type: "button",
-      className: "uppy-u-reset uppy-c-btn",
-      onClick: getFolder,
-    }, title),
-    !isLast ? " / " : "",
-  );
-};
 export default function Breadcrumbs(props) {
   const {
-    getFolder,
+    openFolder,
     title,
     breadcrumbsIcon,
     breadcrumbs,
@@ -31,13 +14,18 @@ export default function Breadcrumbs(props) {
     h("div", {
       className: "uppy-Provider-breadcrumbsIcon",
     }, breadcrumbsIcon),
-    breadcrumbs.map((directory, i) =>
-      h(Breadcrumb, {
-        key: directory.id,
-        getFolder: () => getFolder(directory.requestPath, directory.name),
-        title: i === 0 ? title : directory.name,
-        isLast: i + 1 === breadcrumbs.length,
-      })
+    breadcrumbs.map((folder, index) =>
+      h(
+        Fragment,
+        null,
+        h("button", {
+          key: folder.id,
+          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/lib/Browser.js b/packages/@uppy/provider-views/lib/Browser.js
index 60ad2f8..4df556b 100644
--- a/packages/@uppy/provider-views/lib/Browser.js
+++ b/packages/@uppy/provider-views/lib/Browser.js
@@ -1,205 +1,91 @@
-import remoteFileObjToLocal from "@uppy/utils/lib/remoteFileObjToLocal";
 import VirtualList from "@uppy/utils/lib/VirtualList";
-import classNames from "classnames";
 import { h } from "preact";
-import { useMemo } from "preact/hooks";
-import FooterActions from "./FooterActions.js";
+import { useEffect, useState } from "preact/hooks";
 import Item from "./Item/index.js";
-import SearchFilterInput from "./SearchFilterInput.js";
-const VIRTUAL_SHARED_DIR = "shared-with-me";
-function ListItem(props) {
-  const {
-    currentSelection,
-    uppyFiles,
-    viewType,
-    isChecked,
-    toggleCheckbox,
-    recordShiftKeyPress,
-    showTitles,
-    i18n,
-    validateRestrictions,
-    getNextFolder,
-    f,
-  } = props;
-  if (f.isFolder) {
-    return Item({
-      showTitles,
-      viewType,
-      i18n,
-      id: f.id,
-      title: f.name,
-      getItemIcon: () => f.icon,
-      isChecked: isChecked(f),
-      toggleCheckbox: event => toggleCheckbox(event, f),
-      recordShiftKeyPress,
-      type: "folder",
-      isDisabled: false,
-      isCheckboxDisabled: f.id === VIRTUAL_SHARED_DIR,
-      handleFolderClick: () => getNextFolder(f),
-    });
-  }
-  const restrictionError = validateRestrictions(remoteFileObjToLocal(f), [...uppyFiles, ...currentSelection]);
-  return Item({
-    id: f.id,
-    title: f.name,
-    author: f.author,
-    getItemIcon: () => f.icon,
-    isChecked: isChecked(f),
-    toggleCheckbox: event => toggleCheckbox(event, f),
-    isCheckboxDisabled: false,
-    recordShiftKeyPress,
-    showTitles,
-    viewType,
-    i18n,
-    type: "file",
-    isDisabled: Boolean(restrictionError) && !isChecked(f),
-    restrictionError,
-  });
-}
 function Browser(props) {
   const {
-    currentSelection,
-    folders,
-    files,
-    uppyFiles,
+    displayedPartialTree,
     viewType,
-    headerComponent,
-    showBreadcrumbs,
-    isChecked,
     toggleCheckbox,
-    recordShiftKeyPress,
     handleScroll,
     showTitles,
     i18n,
-    validateRestrictions,
     isLoading,
-    showSearchFilter,
-    search,
-    searchTerm,
-    clearSearch,
-    searchOnInput,
-    searchInputLabel,
-    clearSearchLabel,
-    getNextFolder,
-    cancel,
-    done,
+    openFolder,
     noResultsLabel,
     loadAllFiles,
   } = props;
-  const selected = currentSelection.length;
-  const rows = useMemo(() => [...folders, ...files], [folders, files]);
-  return h(
-    "div",
-    {
-      className: classNames("uppy-ProviderBrowser", `uppy-ProviderBrowser-viewType--${viewType}`),
-    },
-    headerComponent && h(
-      "div",
-      {
-        className: "uppy-ProviderBrowser-header",
+  const [isShiftKeyPressed, setIsShiftKeyPressed] = useState(false);
+  useEffect(() => {
+    const handleKeyUp = e => {
+      if (e.key == "Shift") setIsShiftKeyPressed(false);
+    };
+    const handleKeyDown = e => {
+      if (e.key == "Shift") setIsShiftKeyPressed(true);
+    };
+    document.addEventListener("keyup", handleKeyUp);
+    document.addEventListener("keydown", handleKeyDown);
+    return () => {
+      document.removeEventListener("keyup", handleKeyUp);
+      document.removeEventListener("keydown", handleKeyDown);
+    };
+  }, []);
+  if (isLoading) {
+    return h("div", {
+      className: "uppy-Provider-loading",
+    }, h("span", null, i18n("loading")));
+  }
+  if (displayedPartialTree.length === 0) {
+    return h("div", {
+      className: "uppy-Provider-empty",
+    }, noResultsLabel);
+  }
+  const renderItem = item =>
+    h(Item, {
+      viewType: viewType,
+      toggleCheckbox: event => {
+        var _document$getSelectio;
+        event.stopPropagation();
+        event.preventDefault();
+        (_document$getSelectio = document.getSelection()) == null || _document$getSelectio.removeAllRanges();
+        toggleCheckbox(item, isShiftKeyPressed);
       },
-      h("div", {
-        className: classNames(
-          "uppy-ProviderBrowser-headerBar",
-          !showBreadcrumbs && "uppy-ProviderBrowser-headerBar--simple",
-        ),
-      }, headerComponent),
-    ),
-    showSearchFilter && h(
+      showTitles: showTitles,
+      i18n: i18n,
+      openFolder: openFolder,
+      file: item,
+    });
+  if (loadAllFiles) {
+    return h(
       "div",
       {
-        class: "uppy-ProviderBrowser-searchFilter",
+        className: "uppy-ProviderBrowser-body",
       },
-      h(SearchFilterInput, {
-        search: search,
-        searchTerm: searchTerm,
-        clearSearch: clearSearch,
-        inputLabel: searchInputLabel,
-        clearSearchLabel: clearSearchLabel,
-        inputClassName: "uppy-ProviderBrowser-searchFilterInput",
-        searchOnInput: searchOnInput,
-      }),
-    ),
-    (() => {
-      if (isLoading) {
-        return h("div", {
-          className: "uppy-Provider-loading",
-        }, h("span", null, typeof isLoading === "string" ? isLoading : i18n("loading")));
-      }
-      if (!folders.length && !files.length) {
-        return h("div", {
-          className: "uppy-Provider-empty",
-        }, noResultsLabel);
-      }
-      if (loadAllFiles) {
-        return h(
-          "div",
-          {
-            className: "uppy-ProviderBrowser-body",
-          },
-          h(
-            "ul",
-            {
-              className: "uppy-ProviderBrowser-list",
-            },
-            h(VirtualList, {
-              data: rows,
-              renderRow: f =>
-                h(ListItem, {
-                  currentSelection: currentSelection,
-                  uppyFiles: uppyFiles,
-                  viewType: viewType,
-                  isChecked: isChecked,
-                  toggleCheckbox: toggleCheckbox,
-                  recordShiftKeyPress: recordShiftKeyPress,
-                  showTitles: showTitles,
-                  i18n: i18n,
-                  validateRestrictions: validateRestrictions,
-                  getNextFolder: getNextFolder,
-                  f: f,
-                }),
-              rowHeight: 31,
-            }),
-          ),
-        );
-      }
-      return h(
-        "div",
+      h(
+        "ul",
         {
-          className: "uppy-ProviderBrowser-body",
+          className: "uppy-ProviderBrowser-list",
         },
-        h(
-          "ul",
-          {
-            className: "uppy-ProviderBrowser-list",
-            onScroll: handleScroll,
-            role: "listbox",
-            tabIndex: -1,
-          },
-          rows.map(f =>
-            h(ListItem, {
-              currentSelection: currentSelection,
-              uppyFiles: uppyFiles,
-              viewType: viewType,
-              isChecked: isChecked,
-              toggleCheckbox: toggleCheckbox,
-              recordShiftKeyPress: recordShiftKeyPress,
-              showTitles: showTitles,
-              i18n: i18n,
-              validateRestrictions: validateRestrictions,
-              getNextFolder: getNextFolder,
-              f: f,
-            })
-          ),
-        ),
-      );
-    })(),
-    selected > 0 && h(FooterActions, {
-      selected: selected,
-      done: done,
-      cancel: cancel,
-      i18n: i18n,
-    }),
-  );
+        h(VirtualList, {
+          data: displayedPartialTree,
+          renderRow: renderItem,
+          rowHeight: 31,
+        }),
+      ),
+    );
+  } else {
+    return h(
+      "div",
+      {
+        className: "uppy-ProviderBrowser-body",
+      },
+      h("ul", {
+        className: "uppy-ProviderBrowser-list",
+        onScroll: handleScroll,
+        role: "listbox",
+        tabIndex: -1,
+      }, displayedPartialTree.map(renderItem)),
+    );
+  }
 }
 export default Browser;
diff --git a/packages/@uppy/provider-views/lib/FooterActions.js b/packages/@uppy/provider-views/lib/FooterActions.js
index a57241b..8a49fcb 100644
--- a/packages/@uppy/provider-views/lib/FooterActions.js
+++ b/packages/@uppy/provider-views/lib/FooterActions.js
@@ -1,31 +1,56 @@
+import classNames from "classnames";
 import { h } from "preact";
+import { useMemo } from "preact/hooks";
+import getNOfSelectedFiles from "./utils/PartialTreeUtils/getNOfSelectedFiles";
 export default function FooterActions(_ref) {
   let {
-    cancel,
-    done,
+    cancelSelection,
+    donePicking,
     i18n,
-    selected,
+    partialTree,
+    validateAggregateRestrictions,
   } = _ref;
+  const aggregateRestrictionError = useMemo(() => {
+    return validateAggregateRestrictions(partialTree);
+  }, [partialTree]);
+  const nOfSelectedFiles = useMemo(() => {
+    return getNOfSelectedFiles(partialTree);
+  }, [partialTree]);
+  if (nOfSelectedFiles === 0) {
+    return null;
+  }
   return h(
     "div",
     {
       className: "uppy-ProviderBrowser-footer",
     },
     h(
-      "button",
+      "div",
       {
-        className: "uppy-u-reset uppy-c-btn uppy-c-btn-primary",
-        onClick: done,
-        type: "button",
+        className: "uppy-ProviderBrowser-footer-buttons",
       },
-      i18n("selectX", {
-        smart_count: selected,
-      }),
+      h(
+        "button",
+        {
+          className: classNames("uppy-u-reset uppy-c-btn uppy-c-btn-primary", {
+            "uppy-c-btn--disabled": aggregateRestrictionError,
+          }),
+          disabled: !!aggregateRestrictionError,
+          onClick: donePicking,
+          type: "button",
+        },
+        i18n("selectX", {
+          smart_count: nOfSelectedFiles,
+        }),
+      ),
+      h("button", {
+        className: "uppy-u-reset uppy-c-btn uppy-c-btn-link",
+        onClick: cancelSelection,
+        type: "button",
+      }, i18n("cancel")),
     ),
-    h("button", {
-      className: "uppy-u-reset uppy-c-btn uppy-c-btn-link",
-      onClick: cancel,
-      type: "button",
-    }, i18n("cancel")),
+    aggregateRestrictionError && h("div", {
+      className: "uppy-ProviderBrowser-footer-error",
+    }, aggregateRestrictionError),
   );
 }
diff --git a/packages/@uppy/provider-views/lib/Item/index.js b/packages/@uppy/provider-views/lib/Item/index.js
index e7e4478..7ff61d7 100644
--- a/packages/@uppy/provider-views/lib/Item/index.js
+++ b/packages/@uppy/provider-views/lib/Item/index.js
@@ -1,68 +1,68 @@
-function _extends() {
-  _extends = Object.assign ? Object.assign.bind() : function(target) {
-    for (var i = 1; i < arguments.length; i++) {
-      var source = arguments[i];
-      for (var key in source) if (Object.prototype.hasOwnProperty.call(source, key)) target[key] = source[key];
-    }
-    return target;
-  };
-  return _extends.apply(this, arguments);
-}
 import classNames from "classnames";
 import { h } from "preact";
-import GridListItem from "./components/GridLi.js";
+import GridItem from "./components/GridItem.js";
 import ItemIcon from "./components/ItemIcon.js";
-import ListItem from "./components/ListLi.js";
+import ListItem from "./components/ListItem.js";
 export default function Item(props) {
   const {
-    author,
-    getItemIcon,
-    isChecked,
-    isDisabled,
     viewType,
+    toggleCheckbox,
+    showTitles,
+    i18n,
+    openFolder,
+    file,
   } = props;
-  const itemIconString = getItemIcon();
-  const className = classNames("uppy-ProviderBrowserItem", {
-    "uppy-ProviderBrowserItem--selected": isChecked,
-  }, {
-    "uppy-ProviderBrowserItem--disabled": isDisabled,
-  }, {
-    "uppy-ProviderBrowserItem--noPreview": itemIconString === "video",
-  });
-  const itemIconEl = h(ItemIcon, {
-    itemIconString: itemIconString,
-  });
+  const restrictionError = file.type === "folder" ? null : file.restrictionError;
+  const isDisabled = Boolean(restrictionError) && file.status !== "checked";
+  const sharedProps = {
+    id: file.id,
+    title: file.data.name,
+    status: file.status,
+    i18n,
+    toggleCheckbox,
+    viewType,
+    showTitles,
+    className: classNames("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: h(ItemIcon, {
+      itemIconString: file.data.icon,
+    }),
+    isDisabled,
+    restrictionError,
+  };
+  let ourProps = file.data.isFolder
+    ? {
+      ...sharedProps,
+      type: "folder",
+      handleFolderClick: () => openFolder(file.id),
+    }
+    : {
+      ...sharedProps,
+      type: "file",
+    };
   switch (viewType) {
     case "grid":
-      return h(
-        GridListItem,
-        _extends({}, props, {
-          className: className,
-          itemIconEl: itemIconEl,
-        }),
-      );
+      return h(GridItem, ourProps);
     case "list":
-      return h(
-        ListItem,
-        _extends({}, props, {
-          className: className,
-          itemIconEl: itemIconEl,
-        }),
-      );
+      return h(ListItem, ourProps);
     case "unsplash":
       return h(
-        GridListItem,
-        _extends({}, props, {
-          className: className,
-          itemIconEl: itemIconEl,
-        }),
+        GridItem,
+        ourProps,
         h("a", {
-          href: `${author.url}?utm_source=Companion&utm_medium=referral`,
+          href: `${file.data.author.url}?utm_source=Companion&utm_medium=referral`,
           target: "_blank",
           rel: "noopener noreferrer",
           className: "uppy-ProviderBrowserItem-author",
           tabIndex: -1,
-        }, author.name),
+        }, file.data.author.name),
       );
     default:
       throw new Error(`There is no such type ${viewType}`);
diff --git a/packages/@uppy/provider-views/lib/ProviderView/Header.js b/packages/@uppy/provider-views/lib/ProviderView/Header.js
index b0aa7bc..aa73a46 100644
--- a/packages/@uppy/provider-views/lib/ProviderView/Header.js
+++ b/packages/@uppy/provider-views/lib/ProviderView/Header.js
@@ -1,20 +1,32 @@
-import { Fragment, h } from "preact";
+import classNames from "classnames";
+import { h } from "preact";
 import Breadcrumbs from "../Breadcrumbs.js";
 import User from "./User.js";
 export default function Header(props) {
   return h(
-    Fragment,
-    null,
-    props.showBreadcrumbs && h(Breadcrumbs, {
-      getFolder: props.getFolder,
-      breadcrumbs: props.breadcrumbs,
-      breadcrumbsIcon: props.pluginIcon && props.pluginIcon(),
-      title: props.title,
-    }),
-    h(User, {
-      logout: props.logout,
-      username: props.username,
-      i18n: props.i18n,
-    }),
+    "div",
+    {
+      className: "uppy-ProviderBrowser-header",
+    },
+    h(
+      "div",
+      {
+        className: classNames(
+          "uppy-ProviderBrowser-headerBar",
+          !props.showBreadcrumbs && "uppy-ProviderBrowser-headerBar--simple",
+        ),
+      },
+      props.showBreadcrumbs && h(Breadcrumbs, {
+        openFolder: props.openFolder,
+        breadcrumbs: props.breadcrumbs,
+        breadcrumbsIcon: props.pluginIcon && props.pluginIcon(),
+        title: props.title,
+      }),
+      h(User, {
+        logout: props.logout,
+        username: props.username,
+        i18n: props.i18n,
+      }),
+    ),
   );
 }
diff --git a/packages/@uppy/provider-views/lib/ProviderView/ProviderView.js b/packages/@uppy/provider-views/lib/ProviderView/ProviderView.js
index 670029d..0c2d4f7 100644
--- a/packages/@uppy/provider-views/lib/ProviderView/ProviderView.js
+++ b/packages/@uppy/provider-views/lib/ProviderView/ProviderView.js
@@ -8,24 +8,23 @@ var id = 0;
 function _classPrivateFieldLooseKey(name) {
   return "__private_" + id++ + "_" + name;
 }
-import { getSafeFileId } from "@uppy/utils/lib/generateFileID";
-import PQueue from "p-queue";
 import { h } from "preact";
 import Browser from "../Browser.js";
-import CloseWrapper from "../CloseWrapper.js";
-import View from "../View.js";
 import AuthView from "./AuthView.js";
 import Header from "./Header.js";
 const packageJson = {
   "version": "4.0.0-beta.6",
 };
-function formatBreadcrumbs(breadcrumbs) {
-  return breadcrumbs.slice(1).map(directory => directory.name).join("/");
-}
-function prependPath(path, component) {
-  if (!path) return component;
-  return `${path}/${component}`;
-}
+import remoteFileObjToLocal from "@uppy/utils/lib/remoteFileObjToLocal";
+import classNames from "classnames";
+import FooterActions from "../FooterActions.js";
+import SearchFilterInput from "../SearchFilterInput.js";
+import addFiles from "../utils/addFiles.js";
+import getClickedRange from "../utils/getClickedRange.js";
+import handleError from "../utils/handleError.js";
+import PartialTreeUtils from "../utils/PartialTreeUtils";
+import getCheckedFilesWithPaths from "../utils/PartialTreeUtils/getCheckedFilesWithPaths.js";
+import shouldHandleScroll from "../utils/shouldHandleScroll.js";
 export function defaultPickerIcon() {
   return h(
     "svg",
@@ -41,305 +40,277 @@ export function defaultPickerIcon() {
     }),
   );
 }
-const defaultOptions = {
-  viewType: "list",
-  showTitles: true,
-  showFilter: true,
-  showBreadcrumbs: true,
-  loadAllFiles: false,
-};
+const getDefaultState = rootFolderId => ({
+  authenticated: undefined,
+  partialTree: [{
+    type: "root",
+    id: rootFolderId,
+    cached: false,
+    nextPagePath: null,
+  }],
+  currentFolderId: rootFolderId,
+  searchString: "",
+  didFirstRender: false,
+  username: null,
+  loading: false,
+});
 var _abortController = _classPrivateFieldLooseKey("abortController");
 var _withAbort = _classPrivateFieldLooseKey("withAbort");
-var _list = _classPrivateFieldLooseKey("list");
-var _listFilesAndFolders = _classPrivateFieldLooseKey("listFilesAndFolders");
-var _recursivelyListAllFiles = _classPrivateFieldLooseKey("recursivelyListAllFiles");
-export default class ProviderView extends View {
+export default class ProviderView {
   constructor(plugin, opts) {
-    super(plugin, {
-      ...defaultOptions,
-      ...opts,
-    });
-    Object.defineProperty(this, _recursivelyListAllFiles, {
-      value: _recursivelyListAllFiles2,
-    });
-    Object.defineProperty(this, _listFilesAndFolders, {
-      value: _listFilesAndFolders2,
-    });
-    Object.defineProperty(this, _list, {
-      value: _list2,
-    });
     Object.defineProperty(this, _withAbort, {
       value: _withAbort2,
     });
+    this.isHandlingScroll = false;
+    this.lastCheckbox = null;
     Object.defineProperty(this, _abortController, {
       writable: true,
       value: void 0,
     });
-    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.validateSingleFile = file => {
+      const companionFile = remoteFileObjToLocal(file);
+      const result = this.plugin.uppy.validateSingleFile(companionFile);
+      return result;
+    };
+    this.getBreadcrumbs = () => {
+      const {
+        partialTree,
+        currentFolderId,
+      } = this.plugin.getPluginState();
+      if (!currentFolderId) return [];
+      const breadcrumbs = [];
+      let parent = partialTree.find(folder => folder.id === currentFolderId);
+      while (true) {
+        breadcrumbs.push(parent);
+        if (parent.type === "root") break;
+        parent = partialTree.find(folder => folder.id === parent.parentId);
+      }
+      return breadcrumbs.toReversed();
+    };
+    this.getDisplayedPartialTree = () => {
+      const {
+        partialTree,
+        currentFolderId,
+        searchString,
+      } = this.plugin.getPluginState();
+      const inThisFolder = partialTree.filter(item => item.type !== "root" && item.parentId === currentFolderId);
+      const filtered = searchString === ""
+        ? inThisFolder
+        : inThisFolder.filter(item => item.data.name.toLowerCase().indexOf(searchString.toLowerCase()) !== -1);
+      return filtered;
+    };
+    this.validateAggregateRestrictions = partialTree => {
+      const checkedFiles = partialTree.filter(item => item.type === "file" && item.status === "checked");
+      const uppyFiles = checkedFiles.map(file => file.data);
+      return this.plugin.uppy.validateAggregateRestrictions(uppyFiles);
+    };
+    this.plugin = plugin;
+    this.provider = opts.provider;
+    const defaultOptions = {
+      viewType: "list",
+      showTitles: true,
+      showFilter: true,
+      showBreadcrumbs: true,
+      loadAllFiles: false,
+    };
+    this.opts = {
+      ...defaultOptions,
+      ...opts,
+    };
+    this.openFolder = this.openFolder.bind(this);
     this.logout = this.logout.bind(this);
     this.handleAuth = this.handleAuth.bind(this);
     this.handleScroll = this.handleScroll.bind(this);
+    this.resetPluginState = this.resetPluginState.bind(this);
     this.donePicking = this.donePicking.bind(this);
     this.render = this.render.bind(this);
-    this.plugin.setPluginState({
-      authenticated: undefined,
-      files: [],
-      folders: [],
-      breadcrumbs: [],
-      filterInput: "",
-      isSearchVisible: false,
-      currentSelection: [],
-    });
-    this.registerRequestClient();
-  }
-  tearDown() {}
-  async getFolder(requestPath, name) {
-    this.setLoading(true);
-    try {
-      await _classPrivateFieldLooseBase(this, _withAbort)[_withAbort](async signal => {
-        this.lastCheckbox = undefined;
-        let {
-          breadcrumbs,
-        } = this.plugin.getPluginState();
-        const index = breadcrumbs.findIndex(dir => requestPath === dir.requestPath);
-        if (index !== -1) {
-          breadcrumbs = breadcrumbs.slice(0, index + 1);
-        } else {
-          breadcrumbs = [...breadcrumbs, {
-            requestPath,
-            name,
-          }];
-        }
-        this.nextPagePath = requestPath;
-        let files = [];
-        let folders = [];
-        do {
-          const {
-            files: newFiles,
-            folders: newFolders,
-          } = await _classPrivateFieldLooseBase(this, _listFilesAndFolders)[_listFilesAndFolders]({
-            breadcrumbs,
-            signal,
-          });
-          files = files.concat(newFiles);
-          folders = folders.concat(newFolders);
-          this.setLoading(this.plugin.uppy.i18n("loadedXFiles", {
-            numFiles: files.length + folders.length,
-          }));
-        } while (this.opts.loadAllFiles && this.nextPagePath);
-        this.plugin.setPluginState({
-          folders,
-          files,
-          breadcrumbs,
-          filterInput: "",
-        });
-      });
-    } catch (err) {
-      if ((err == null ? void 0 : err.name) === "UserFacingApiError") {
-        this.plugin.uppy.info(
-          {
-            message: this.plugin.uppy.i18n(err.message),
-          },
-          "warning",
-          5000,
-        );
-        return;
-      }
-      this.handleError(err);
-    } finally {
-      this.setLoading(false);
-    }
-  }
-  getNextFolder(folder) {
-    this.getFolder(folder.requestPath, folder.name);
-    this.lastCheckbox = undefined;
+    this.cancelSelection = this.cancelSelection.bind(this);
+    this.toggleCheckbox = this.toggleCheckbox.bind(this);
+    this.resetPluginState();
+    this.plugin.uppy.on("dashboard:close-panel", this.resetPluginState);
+    this.plugin.uppy.registerRequestClient(this.provider.provider, this.provider);
   }
-  async logout() {
-    try {
-      await _classPrivateFieldLooseBase(this, _withAbort)[_withAbort](async signal => {
-        const res = await this.provider.logout({
-          signal,
-        });
-        if (res.ok) {
-          if (!res.revoked) {
-            const message = this.plugin.uppy.i18n("companionUnauthorizeHint", {
-              provider: this.plugin.title,
-              url: res.manual_revoke_url,
-            });
-            this.plugin.uppy.info(message, "info", 7000);
-          }
-          const newState = {
-            authenticated: false,
-            files: [],
-            folders: [],
-            breadcrumbs: [],
-            filterInput: "",
-          };
-          this.plugin.setPluginState(newState);
-        }
-      });
-    } catch (err) {
-      this.handleError(err);
-    }
+  resetPluginState() {
+    this.plugin.setPluginState(getDefaultState(this.plugin.rootFolderId));
   }
-  filterQuery(input) {
+  tearDown() {}
+  setLoading(loading) {
     this.plugin.setPluginState({
-      filterInput: input,
+      loading,
     });
   }
-  clearFilter() {
+  cancelSelection() {
+    const {
+      partialTree,
+    } = this.plugin.getPluginState();
+    const newPartialTree = partialTree.map(item =>
+      item.type === "root" ? item : {
+        ...item,
+        status: "unchecked",
+      }
+    );
     this.plugin.setPluginState({
-      filterInput: "",
+      partialTree: newPartialTree,
     });
   }
-  async handleAuth(authFormData) {
-    try {
-      await _classPrivateFieldLooseBase(this, _withAbort)[_withAbort](async signal => {
-        this.setLoading(true);
-        await this.provider.login({
-          authFormData,
+  async openFolder(folderId) {
+    this.lastCheckbox = null;
+    console.log(`____________________________________________GETTING FOLDER "${folderId}"`);
+    const {
+      partialTree,
+    } = this.plugin.getPluginState();
+    const clickedFolder = partialTree.find(folder => folder.id === folderId);
+    if (clickedFolder.cached) {
+      console.log("Folder was cached____________________________________________");
+      this.plugin.setPluginState({
+        currentFolderId: folderId,
+        searchString: "",
+      });
+      return;
+    }
+    this.setLoading(true);
+    await _classPrivateFieldLooseBase(this, _withAbort)[_withAbort](async signal => {
+      let currentPagePath = folderId;
+      let currentItems = [];
+      do {
+        const {
+          username,
+          nextPagePath,
+          items,
+        } = await this.provider.list(currentPagePath, {
           signal,
         });
         this.plugin.setPluginState({
-          authenticated: true,
+          username,
         });
-        await this.getFolder(this.plugin.rootFolderId || undefined);
+        currentPagePath = nextPagePath;
+        currentItems = currentItems.concat(items);
+        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,
+      );
+      this.plugin.setPluginState({
+        partialTree: newPartialTree,
+        currentFolderId: folderId,
+        searchString: "",
       });
-    } catch (err) {
-      if (err.name === "UserFacingApiError") {
-        this.plugin.uppy.info(
-          {
-            message: this.plugin.uppy.i18n(err.message),
-          },
-          "warning",
-          5000,
-        );
-        return;
+    }).catch(handleError(this.plugin.uppy));
+    this.setLoading(false);
+  }
+  async logout() {
+    await _classPrivateFieldLooseBase(this, _withAbort)[_withAbort](async signal => {
+      const res = await this.provider.logout({
+        signal,
+      });
+      if (res.ok) {
+        if (!res.revoked) {
+          const message = this.plugin.uppy.i18n("companionUnauthorizeHint", {
+            provider: this.plugin.title,
+            url: res.manual_revoke_url,
+          });
+          this.plugin.uppy.info(message, "info", 7000);
+        }
+        this.plugin.setPluginState({
+          ...getDefaultState(this.plugin.rootFolderId),
+          authenticated: false,
+        });
       }
-      this.plugin.uppy.log(`login failed: ${err.message}`);
-    } finally {
-      this.setLoading(false);
-    }
+    }).catch(handleError(this.plugin.uppy));
+  }
+  async handleAuth(authFormData) {
+    await _classPrivateFieldLooseBase(this, _withAbort)[_withAbort](async signal => {
+      this.setLoading(true);
+      await this.provider.login({
+        authFormData,
+        signal,
+      });
+      this.plugin.setPluginState({
+        authenticated: true,
+      });
+      await Promise.all([this.provider.fetchPreAuthToken(), this.openFolder(this.plugin.rootFolderId)]);
+    }).catch(handleError(this.plugin.uppy));
+    this.setLoading(false);
   }
   async handleScroll(event) {
-    if (this.shouldHandleScroll(event) && this.nextPagePath) {
+    const {
+      partialTree,
+      currentFolderId,
+    } = this.plugin.getPluginState();
+    const currentFolder = partialTree.find(i => i.id === currentFolderId);
+    if (shouldHandleScroll(event) && !this.isHandlingScroll && currentFolder.nextPagePath) {
       this.isHandlingScroll = true;
-      try {
-        await _classPrivateFieldLooseBase(this, _withAbort)[_withAbort](async signal => {
-          const {
-            files,
-            folders,
-            breadcrumbs,
-          } = this.plugin.getPluginState();
-          const {
-            files: newFiles,
-            folders: newFolders,
-          } = await _classPrivateFieldLooseBase(this, _listFilesAndFolders)[_listFilesAndFolders]({
-            breadcrumbs,
-            signal,
-          });
-          const combinedFiles = files.concat(newFiles);
-          const combinedFolders = folders.concat(newFolders);
-          this.plugin.setPluginState({
-            folders: combinedFolders,
-            files: combinedFiles,
-          });
+      await _classPrivateFieldLooseBase(this, _withAbort)[_withAbort](async signal => {
+        const {
+          nextPagePath,
+          items,
+        } = await this.provider.list(currentFolder.nextPagePath, {
+          signal,
         });
-      } catch (error) {
-        this.handleError(error);
-      } finally {
-        this.isHandlingScroll = false;
-      }
+        const newPartialTree = PartialTreeUtils.afterScrollFolder(
+          partialTree,
+          currentFolderId,
+          items,
+          nextPagePath,
+          this.validateSingleFile,
+        );
+        this.plugin.setPluginState({
+          partialTree: newPartialTree,
+        });
+      }).catch(handleError(this.plugin.uppy));
+      this.isHandlingScroll = false;
     }
   }
   async donePicking() {
+    const {
+      partialTree,
+    } = this.plugin.getPluginState();
     this.setLoading(true);
-    try {
-      await _classPrivateFieldLooseBase(this, _withAbort)[_withAbort](async signal => {
-        const {
-          currentSelection,
-        } = this.plugin.getPluginState();
-        const messages = [];
-        const newFiles = [];
-        for (const selectedItem of currentSelection) {
-          const {
-            requestPath,
-          } = selectedItem;
-          const withRelDirPath = newItem => ({
-            ...newItem,
-            relDirPath: newItem.absDirPath.replace(selectedItem.absDirPath, "").replace(/^\//, ""),
-          });
-          if (selectedItem.isFolder) {
-            let isEmpty = true;
-            let numNewFiles = 0;
-            const queue = new PQueue({
-              concurrency: 6,
-            });
-            const onFiles = files => {
-              for (const newFile of files) {
-                const tagFile = this.getTagFile(newFile);
-                const id = getSafeFileId(tagFile, this.plugin.uppy.getID());
-                if (!this.plugin.uppy.checkIfFileAlreadyExists(id)) {
-                  newFiles.push(withRelDirPath(newFile));
-                  numNewFiles++;
-                  this.setLoading(this.plugin.uppy.i18n("addedNumFiles", {
-                    numFiles: numNewFiles,
-                  }));
-                }
-                isEmpty = false;
-              }
-            };
-            await _classPrivateFieldLooseBase(this, _recursivelyListAllFiles)[_recursivelyListAllFiles]({
-              requestPath,
-              absDirPath: prependPath(selectedItem.absDirPath, selectedItem.name),
-              relDirPath: selectedItem.name,
-              queue,
-              onFiles,
-              signal,
-            });
-            await queue.onIdle();
-            let message;
-            if (isEmpty) {
-              message = this.plugin.uppy.i18n("emptyFolderAdded");
-            } else if (numNewFiles === 0) {
-              message = this.plugin.uppy.i18n("folderAlreadyAdded", {
-                folder: selectedItem.name,
-              });
-            } else {
-              message = this.plugin.uppy.i18n("folderAdded", {
-                smart_count: numNewFiles,
-                folder: selectedItem.name,
-              });
-            }
-            messages.push(message);
-          } else {
-            newFiles.push(withRelDirPath(selectedItem));
-          }
-        }
-        this.plugin.uppy.log("Adding files from a remote provider");
-        this.plugin.uppy.addFiles(newFiles.map(file => this.getTagFile(file, this.requestClientId)));
+    await _classPrivateFieldLooseBase(this, _withAbort)[_withAbort](async signal => {
+      const enrichedTree = await PartialTreeUtils.afterFill(partialTree, path =>
+        this.provider.list(path, {
+          signal,
+        }), this.validateSingleFile);
+      const aggregateRestrictionError = this.validateAggregateRestrictions(enrichedTree);
+      if (aggregateRestrictionError) {
         this.plugin.setPluginState({
-          filterInput: "",
+          partialTree: enrichedTree,
         });
-        messages.forEach(message => this.plugin.uppy.info(message));
-        this.clearSelection();
-      });
-    } catch (err) {
-      this.handleError(err);
-    } finally {
-      this.setLoading(false);
-    }
+        return;
+      }
+      const companionFiles = getCheckedFilesWithPaths(enrichedTree);
+      addFiles(companionFiles, this.plugin, this.provider);
+      this.resetPluginState();
+    }).catch(handleError(this.plugin.uppy));
+    this.setLoading(false);
+  }
+  toggleCheckbox(ourItem, isShiftKeyPressed) {
+    const {
+      partialTree,
+    } = this.plugin.getPluginState();
+    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;
   }
   render(state, viewOptions) {
-    var _this = this;
     if (viewOptions === void 0) {
       viewOptions = {};
     }
     const {
-      authenticated,
       didFirstRender,
     } = this.plugin.getPluginState();
     const {
@@ -350,90 +321,79 @@ export default class ProviderView extends View {
         didFirstRender: true,
       });
       this.provider.fetchPreAuthToken();
-      this.getFolder(this.plugin.rootFolderId || undefined);
+      this.openFolder(this.plugin.rootFolderId);
     }
-    const targetViewOptions = {
+    const opts = {
       ...this.opts,
       ...viewOptions,
     };
     const {
-      files,
-      folders,
-      filterInput,
+      authenticated,
+      partialTree,
+      username,
+      searchString,
       loading,
-      currentSelection,
     } = this.plugin.getPluginState();
-    const {
-      isChecked,
-      recordShiftKeyPress,
-      filterItems,
-    } = this;
-    const hasInput = filterInput !== "";
     const pluginIcon = this.plugin.icon || defaultPickerIcon;
-    const headerProps = {
-      showBreadcrumbs: targetViewOptions.showBreadcrumbs,
-      getFolder: this.getFolder,
-      breadcrumbs: this.plugin.getPluginState().breadcrumbs,
-      pluginIcon,
-      title: this.plugin.title,
-      logout: this.logout,
-      username: this.username,
-      i18n,
-    };
-    const browserProps = {
-      isChecked,
-      toggleCheckbox: this.toggleCheckbox.bind(this),
-      recordShiftKeyPress,
-      currentSelection,
-      files: hasInput ? filterItems(files) : files,
-      folders: hasInput ? filterItems(folders) : folders,
-      getNextFolder: this.getNextFolder,
-      getFolder: this.getFolder,
-      loadAllFiles: this.opts.loadAllFiles,
-      showSearchFilter: targetViewOptions.showFilter,
-      search: this.filterQuery,
-      clearSearch: this.clearFilter,
-      searchTerm: filterInput,
-      searchOnInput: true,
-      searchInputLabel: i18n("filter"),
-      clearSearchLabel: i18n("resetFilter"),
-      noResultsLabel: i18n("noFilesFound"),
-      logout: this.logout,
-      handleScroll: this.handleScroll,
-      done: this.donePicking,
-      cancel: this.cancelPicking,
-      headerComponent: h(Header, headerProps),
-      title: this.plugin.title,
-      viewType: targetViewOptions.viewType,
-      showTitles: targetViewOptions.showTitles,
-      showBreadcrumbs: targetViewOptions.showBreadcrumbs,
-      pluginIcon,
-      i18n: this.plugin.uppy.i18n,
-      uppyFiles: this.plugin.uppy.getFiles(),
-      validateRestrictions: function() {
-        return _this.plugin.uppy.validateRestrictions(...arguments);
-      },
-      isLoading: loading,
-    };
     if (authenticated === false) {
-      return h(
-        CloseWrapper,
-        {
-          onUnmount: this.clearSelection,
-        },
-        h(AuthView, {
-          pluginName: this.plugin.title,
-          pluginIcon: pluginIcon,
-          handleAuth: this.handleAuth,
-          i18n: this.plugin.uppy.i18nArray,
-          renderForm: this.opts.renderAuthForm,
-          loading: loading,
-        }),
-      );
+      return h(AuthView, {
+        pluginName: this.plugin.title,
+        pluginIcon: pluginIcon,
+        handleAuth: this.handleAuth,
+        i18n: this.plugin.uppy.i18nArray,
+        renderForm: opts.renderAuthForm,
+        loading: loading,
+      });
     }
-    return h(CloseWrapper, {
-      onUnmount: this.clearSelection,
-    }, h(Browser, browserProps));
+    return h(
+      "div",
+      {
+        className: classNames("uppy-ProviderBrowser", `uppy-ProviderBrowser-viewType--${opts.viewType}`),
+      },
+      h(Header, {
+        showBreadcrumbs: opts.showBreadcrumbs,
+        openFolder: this.openFolder,
+        breadcrumbs: this.getBreadcrumbs(),
+        pluginIcon: pluginIcon,
+        title: this.plugin.title,
+        logout: this.logout,
+        username: username,
+        i18n: i18n,
+      }),
+      opts.showFilter && h(SearchFilterInput, {
+        searchString: searchString,
+        setSearchString: searchString => {
+          console.log("setting searchString!", searchString);
+          this.plugin.setPluginState({
+            searchString,
+          });
+        },
+        submitSearchString: () => {},
+        inputLabel: i18n("filter"),
+        clearSearchLabel: i18n("resetFilter"),
+        wrapperClassName: "uppy-ProviderBrowser-searchFilter",
+        inputClassName: "uppy-ProviderBrowser-searchFilterInput",
+      }),
+      h(Browser, {
+        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,
+      }),
+      h(FooterActions, {
+        partialTree: partialTree,
+        donePicking: this.donePicking,
+        cancelSelection: this.cancelSelection,
+        i18n: i18n,
+        validateAggregateRestrictions: this.validateAggregateRestrictions,
+      }),
+    );
   }
 }
 async function _withAbort2(op) {
@@ -444,7 +404,6 @@ async function _withAbort2(op) {
   _classPrivateFieldLooseBase(this, _abortController)[_abortController] = abortController;
   const cancelRequest = () => {
     abortController.abort();
-    this.clearSelection();
   };
   try {
     this.plugin.uppy.on("dashboard:close-panel", cancelRequest);
@@ -456,90 +415,4 @@ async function _withAbort2(op) {
     _classPrivateFieldLooseBase(this, _abortController)[_abortController] = undefined;
   }
 }
-async function _list2(_ref) {
-  let {
-    requestPath,
-    absDirPath,
-    signal,
-  } = _ref;
-  const {
-    username,
-    nextPagePath,
-    items,
-  } = await this.provider.list(requestPath, {
-    signal,
-  });
-  this.username = username || this.username;
-  return {
-    items: items.map(item => ({
-      ...item,
-      absDirPath,
-    })),
-    nextPagePath,
-  };
-}
-async function _listFilesAndFolders2(_ref2) {
-  let {
-    breadcrumbs,
-    signal,
-  } = _ref2;
-  const absDirPath = formatBreadcrumbs(breadcrumbs);
-  const {
-    items,
-    nextPagePath,
-  } = await _classPrivateFieldLooseBase(this, _list)[_list]({
-    requestPath: this.nextPagePath,
-    absDirPath,
-    signal,
-  });
-  this.nextPagePath = nextPagePath;
-  const files = [];
-  const folders = [];
-  items.forEach(item => {
-    if (item.isFolder) {
-      folders.push(item);
-    } else {
-      files.push(item);
-    }
-  });
-  return {
-    files,
-    folders,
-  };
-}
-async function _recursivelyListAllFiles2(_ref3) {
-  let {
-    requestPath,
-    absDirPath,
-    relDirPath,
-    queue,
-    onFiles,
-    signal,
-  } = _ref3;
-  let curPath = requestPath;
-  while (curPath) {
-    const res = await _classPrivateFieldLooseBase(this, _list)[_list]({
-      requestPath: curPath,
-      absDirPath,
-      signal,
-    });
-    curPath = res.nextPagePath;
-    const files = res.items.filter(item => !item.isFolder);
-    const folders = res.items.filter(item => item.isFolder);
-    onFiles(files);
-    const promises = folders.map(async folder =>
-      queue.add(async () =>
-        _classPrivateFieldLooseBase(this, _recursivelyListAllFiles)[_recursivelyListAllFiles]({
-          requestPath: folder.requestPath,
-          absDirPath: prependPath(absDirPath, folder.name),
-          relDirPath: prependPath(relDirPath, folder.name),
-          queue,
-          onFiles,
-          signal,
-        })
-      )
-    );
-    await Promise.all(promises);
-  }
-}
 ProviderView.VERSION = packageJson.version;
diff --git a/packages/@uppy/provider-views/lib/ProviderView/User.js b/packages/@uppy/provider-views/lib/ProviderView/User.js
index eff0033..22e1d48 100644
--- a/packages/@uppy/provider-views/lib/ProviderView/User.js
+++ b/packages/@uppy/provider-views/lib/ProviderView/User.js
@@ -8,7 +8,7 @@ export default function User(_ref) {
   return h(
     Fragment,
     null,
-    h("span", {
+    username && h("span", {
       className: "uppy-ProviderBrowser-user",
       key: "username",
     }, username),
diff --git a/packages/@uppy/provider-views/lib/SearchFilterInput.js b/packages/@uppy/provider-views/lib/SearchFilterInput.js
index e358931..db9e286 100644
--- a/packages/@uppy/provider-views/lib/SearchFilterInput.js
+++ b/packages/@uppy/provider-views/lib/SearchFilterInput.js
@@ -1,58 +1,37 @@
-import { nanoid } from "nanoid/non-secure";
-import { Fragment, h } from "preact";
-import { useCallback, useEffect, useState } from "preact/hooks";
+import { h } from "preact";
 export default function SearchFilterInput(props) {
   const {
-    search,
-    searchOnInput,
-    searchTerm,
+    searchString,
+    setSearchString,
+    submitSearchString,
     showButton,
     inputLabel,
     clearSearchLabel,
     buttonLabel,
-    clearSearch,
+    wrapperClassName,
     inputClassName,
     buttonCSSClassName,
   } = props;
-  const [searchText, setSearchText] = useState(searchTerm != null ? searchTerm : "");
-  const validateAndSearch = useCallback(ev => {
-    ev.preventDefault();
-    search(searchText);
-  }, [search, searchText]);
-  const handleInput = useCallback(ev => {
-    const inputValue = ev.target.value;
-    setSearchText(inputValue);
-    if (searchOnInput) search(inputValue);
-  }, [setSearchText, searchOnInput, search]);
-  const handleReset = () => {
-    setSearchText("");
-    if (clearSearch) clearSearch();
+  const onSubmit = e => {
+    e.preventDefault();
+    submitSearchString();
+  };
+  const onInput = e => {
+    setSearchString(e.target.value);
   };
-  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]);
   return h(
-    Fragment,
-    null,
+    "form",
+    {
+      className: wrapperClassName,
+      onSubmit: onSubmit,
+    },
     h("input", {
       className: `uppy-u-reset ${inputClassName}`,
       type: "search",
       "aria-label": inputLabel,
       placeholder: inputLabel,
-      value: searchText,
-      onInput: handleInput,
-      form: form.id,
+      value: searchString,
+      onInput: onInput,
       "data-uppy-super-focusable": true,
     }),
     !showButton && h(
@@ -69,14 +48,16 @@ export default function SearchFilterInput(props) {
         d: "M8.638 7.99l3.172 3.172a.492.492 0 1 1-.697.697L7.91 8.656a4.977 4.977 0 0 1-2.983.983C2.206 9.639 0 7.481 0 4.819 0 2.158 2.206 0 4.927 0c2.721 0 4.927 2.158 4.927 4.82a4.74 4.74 0 0 1-1.216 3.17zm-3.71.685c2.176 0 3.94-1.726 3.94-3.856 0-2.129-1.764-3.855-3.94-3.855C2.75.964.984 2.69.984 4.819c0 2.13 1.765 3.856 3.942 3.856z",
       }),
     ),
-    !showButton && searchText && h(
+    !showButton && searchString && h(
       "button",
       {
         className: "uppy-u-reset uppy-ProviderBrowser-searchFilterReset",
         type: "button",
         "aria-label": clearSearchLabel,
         title: clearSearchLabel,
-        onClick: handleReset,
+        onClick: () => {
+          setSearchString("");
+        },
       },
       h(
         "svg",
@@ -94,7 +75,6 @@ export default function SearchFilterInput(props) {
     showButton && h("button", {
       className: `uppy-u-reset uppy-c-btn uppy-c-btn-primary ${buttonCSSClassName}`,
       type: "submit",
-      form: form.id,
     }, buttonLabel),
   );
 }
diff --git a/packages/@uppy/provider-views/lib/SearchProviderView/SearchProviderView.js b/packages/@uppy/provider-views/lib/SearchProviderView/SearchProviderView.js
index c27fa47..6373d2a 100644
--- a/packages/@uppy/provider-views/lib/SearchProviderView/SearchProviderView.js
+++ b/packages/@uppy/provider-views/lib/SearchProviderView/SearchProviderView.js
@@ -1,207 +1,261 @@
-function _classPrivateFieldLooseBase(receiver, privateKey) {
-  if (!Object.prototype.hasOwnProperty.call(receiver, privateKey)) {
-    throw new TypeError("attempted to use private field on non-instance");
-  }
-  return receiver;
-}
-var id = 0;
-function _classPrivateFieldLooseKey(name) {
-  return "__private_" + id++ + "_" + name;
-}
 import { h } from "preact";
 import Browser from "../Browser.js";
-import CloseWrapper from "../CloseWrapper.js";
 import SearchFilterInput from "../SearchFilterInput.js";
-import View from "../View.js";
 const packageJson = {
   "version": "4.0.0-beta.6",
 };
+import remoteFileObjToLocal from "@uppy/utils/lib/remoteFileObjToLocal";
+import classNames from "classnames";
+import FooterActions from "../FooterActions.js";
+import addFiles from "../utils/addFiles.js";
+import getClickedRange from "../utils/getClickedRange.js";
+import handleError from "../utils/handleError.js";
+import PartialTreeUtils from "../utils/PartialTreeUtils";
+import getCheckedFilesWithPaths from "../utils/PartialTreeUtils/getCheckedFilesWithPaths.js";
+import shouldHandleScroll from "../utils/shouldHandleScroll.js";
 const defaultState = {
+  loading: false,
+  searchString: "",
+  partialTree: [{
+    type: "root",
+    id: null,
+    cached: false,
+    nextPagePath: null,
+  }],
+  currentFolderId: null,
   isInputMode: true,
-  files: [],
-  folders: [],
-  breadcrumbs: [],
-  filterInput: "",
-  currentSelection: [],
-  searchTerm: null,
-};
-const defaultOptions = {
-  viewType: "grid",
-  showTitles: true,
-  showFilter: true,
-  showBreadcrumbs: true,
 };
-var _updateFilesAndInputMode = _classPrivateFieldLooseKey("updateFilesAndInputMode");
-export default class SearchProviderView extends View {
+export default class SearchProviderView {
   constructor(plugin, opts) {
-    super(plugin, {
+    this.isHandlingScroll = false;
+    this.lastCheckbox = null;
+    this.validateSingleFile = file => {
+      const companionFile = remoteFileObjToLocal(file);
+      const result = this.plugin.uppy.validateSingleFile(companionFile);
+      return result;
+    };
+    this.getDisplayedPartialTree = () => {
+      const {
+        partialTree,
+      } = this.plugin.getPluginState();
+      return partialTree.filter(item => item.type !== "root");
+    };
+    this.setSearchString = searchString => {
+      this.plugin.setPluginState({
+        searchString,
+      });
+      if (searchString === "") {
+        this.plugin.setPluginState({
+          partialTree: [],
+        });
+      }
+    };
+    this.validateAggregateRestrictions = partialTree => {
+      const checkedFiles = partialTree.filter(item => item.type === "file" && item.status === "checked");
+      const uppyFiles = checkedFiles.map(file => file.data);
+      return this.plugin.uppy.validateAggregateRestrictions(uppyFiles);
+    };
+    this.plugin = plugin;
+    this.provider = opts.provider;
+    const defaultOptions = {
+      viewType: "grid",
+      showTitles: true,
+      showFilter: true,
+    };
+    this.opts = {
       ...defaultOptions,
       ...opts,
-    });
-    Object.defineProperty(this, _updateFilesAndInputMode, {
-      value: _updateFilesAndInputMode2,
-    });
-    this.nextPageQuery = null;
+    };
+    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);
+    this.cancelSelection = this.cancelSelection.bind(this);
+    this.toggleCheckbox = this.toggleCheckbox.bind(this);
     this.render = this.render.bind(this);
-    this.plugin.setPluginState(defaultState);
-    this.registerRequestClient();
+    this.resetPluginState();
+    this.plugin.uppy.on("dashboard:close-panel", this.resetPluginState);
+    this.plugin.uppy.registerRequestClient(this.provider.provider, this.provider);
   }
   tearDown() {}
+  setLoading(loading) {
+    this.plugin.setPluginState({
+      loading,
+    });
+  }
   resetPluginState() {
     this.plugin.setPluginState(defaultState);
   }
-  async search(query) {
+  cancelSelection() {
     const {
-      searchTerm,
+      partialTree,
     } = this.plugin.getPluginState();
-    if (query && query === searchTerm) {
-      return;
-    }
+    const newPartialTree = partialTree.map(item =>
+      item.type === "root" ? item : {
+        ...item,
+        status: "unchecked",
+      }
+    );
+    this.plugin.setPluginState({
+      partialTree: newPartialTree,
+    });
+  }
+  async search() {
+    const {
+      searchString,
+    } = this.plugin.getPluginState();
+    if (searchString === "") return;
     this.setLoading(true);
     try {
-      const res = await this.provider.search(query);
-      _classPrivateFieldLooseBase(this, _updateFilesAndInputMode)[_updateFilesAndInputMode](res, []);
-    } catch (err) {
-      this.handleError(err);
-    } finally {
-      this.setLoading(false);
+      const response = await this.provider.search(searchString);
+      const newPartialTree = [
+        {
+          type: "root",
+          id: null,
+          cached: false,
+          nextPagePath: response.nextPageQuery,
+        },
+        ...response.items.map(item => ({
+          type: "file",
+          id: item.requestPath,
+          status: "unchecked",
+          parentId: null,
+          data: item,
+        })),
+      ];
+      this.plugin.setPluginState({
+        partialTree: newPartialTree,
+        isInputMode: false,
+      });
+    } catch (error) {
+      handleError(this.plugin.uppy)(error);
     }
-  }
-  clearSearch() {
-    this.plugin.setPluginState({
-      currentSelection: [],
-      files: [],
-      searchTerm: null,
-    });
+    this.setLoading(false);
   }
   async handleScroll(event) {
-    const query = this.nextPageQuery || null;
-    if (this.shouldHandleScroll(event) && query) {
+    const {
+      partialTree,
+      searchString,
+    } = this.plugin.getPluginState();
+    const root = partialTree.find(i => i.type === "root");
+    if (shouldHandleScroll(event) && !this.isHandlingScroll && root.nextPagePath) {
       this.isHandlingScroll = true;
       try {
-        const {
-          files,
-          searchTerm,
-        } = this.plugin.getPluginState();
-        const response = await this.provider.search(searchTerm, query);
-        _classPrivateFieldLooseBase(this, _updateFilesAndInputMode)[_updateFilesAndInputMode](response, files);
+        const response = await this.provider.search(searchString, root.nextPagePath);
+        const newRoot = {
+          ...root,
+          nextPagePath: response.nextPageQuery,
+        };
+        const oldItems = partialTree.filter(i => i.type !== "root");
+        const newPartialTree = [
+          newRoot,
+          ...oldItems,
+          ...response.items.map(item => ({
+            type: "file",
+            id: item.requestPath,
+            status: "unchecked",
+            parentId: null,
+            data: item,
+          })),
+        ];
+        this.plugin.setPluginState({
+          partialTree: newPartialTree,
+        });
       } catch (error) {
-        this.handleError(error);
-      } finally {
-        this.isHandlingScroll = false;
+        handleError(this.plugin.uppy)(error);
       }
+      this.isHandlingScroll = false;
     }
   }
-  donePicking() {
+  async donePicking() {
     const {
-      currentSelection,
+      partialTree,
     } = this.plugin.getPluginState();
-    this.plugin.uppy.log("Adding remote search provider files");
-    this.plugin.uppy.addFiles(currentSelection.map(file => this.getTagFile(file)));
+    const companionFiles = getCheckedFilesWithPaths(partialTree);
+    addFiles(companionFiles, this.plugin, this.provider);
     this.resetPluginState();
   }
+  toggleCheckbox(ourItem, isShiftKeyPressed) {
+    const {
+      partialTree,
+    } = this.plugin.getPluginState();
+    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;
+  }
   render(state, viewOptions) {
-    var _this = this;
     if (viewOptions === void 0) {
       viewOptions = {};
     }
     const {
       isInputMode,
-      searchTerm,
+      searchString,
+      loading,
+      partialTree,
     } = this.plugin.getPluginState();
     const {
       i18n,
     } = this.plugin.uppy;
-    const targetViewOptions = {
+    const opts = {
       ...this.opts,
       ...viewOptions,
     };
-    const {
-      files,
-      folders,
-      filterInput,
-      loading,
-      currentSelection,
-    } = this.plugin.getPluginState();
-    const {
-      isChecked,
-      filterItems,
-      recordShiftKeyPress,
-    } = this;
-    const hasInput = filterInput !== "";
-    const browserProps = {
-      isChecked,
-      toggleCheckbox: this.toggleCheckbox.bind(this),
-      recordShiftKeyPress,
-      currentSelection,
-      files: hasInput ? filterItems(files) : files,
-      folders: hasInput ? filterItems(folders) : folders,
-      handleScroll: this.handleScroll,
-      done: this.donePicking,
-      cancel: this.cancelPicking,
-      showSearchFilter: targetViewOptions.showFilter,
-      search: this.search,
-      clearSearch: this.clearSearch,
-      searchTerm,
-      searchOnInput: false,
-      searchInputLabel: i18n("search"),
-      clearSearchLabel: i18n("resetSearch"),
-      noResultsLabel: i18n("noSearchResults"),
-      title: this.plugin.title,
-      viewType: targetViewOptions.viewType,
-      showTitles: targetViewOptions.showTitles,
-      showFilter: targetViewOptions.showFilter,
-      isLoading: loading,
-      showBreadcrumbs: targetViewOptions.showBreadcrumbs,
-      pluginIcon: this.plugin.icon,
-      i18n,
-      uppyFiles: this.plugin.uppy.getFiles(),
-      validateRestrictions: function() {
-        return _this.plugin.uppy.validateRestrictions(...arguments);
-      },
-    };
     if (isInputMode) {
-      return h(
-        CloseWrapper,
-        {
-          onUnmount: this.resetPluginState,
-        },
-        h(
-          "div",
-          {
-            className: "uppy-SearchProvider",
-          },
-          h(SearchFilterInput, {
-            search: this.search,
-            inputLabel: i18n("enterTextToSearch"),
-            buttonLabel: i18n("searchImages"),
-            inputClassName: "uppy-c-textInput uppy-SearchProvider-input",
-            buttonCSSClassName: "uppy-SearchProvider-searchButton",
-            showButton: true,
-          }),
-        ),
-      );
+      return h(SearchFilterInput, {
+        searchString: searchString,
+        setSearchString: this.setSearchString,
+        submitSearchString: this.search,
+        inputLabel: i18n("enterTextToSearch"),
+        buttonLabel: i18n("searchImages"),
+        wrapperClassName: "uppy-SearchProvider",
+        inputClassName: "uppy-c-textInput uppy-SearchProvider-input",
+        buttonCSSClassName: "uppy-SearchProvider-searchButton",
+        showButton: true,
+      });
     }
-    return h(CloseWrapper, {
-      onUnmount: this.resetPluginState,
-    }, h(Browser, browserProps));
+    return h(
+      "div",
+      {
+        className: classNames("uppy-ProviderBrowser", `uppy-ProviderBrowser-viewType--${opts.viewType}`),
+      },
+      opts.showFilter && h(SearchFilterInput, {
+        searchString: searchString,
+        setSearchString: this.setSearchString,
+        submitSearchString: this.search,
+        inputLabel: i18n("search"),
+        clearSearchLabel: i18n("resetSearch"),
+        wrapperClassName: "uppy-ProviderBrowser-searchFilter",
+        inputClassName: "uppy-ProviderBrowser-searchFilterInput",
+      }),
+      h(Browser, {
+        toggleCheckbox: this.toggleCheckbox,
+        displayedPartialTree: this.getDisplayedPartialTree(),
+        handleScroll: this.handleScroll,
+        openFolder: async () => {},
+        noResultsLabel: i18n("noSearchResults"),
+        viewType: opts.viewType,
+        showTitles: opts.showTitles,
+        isLoading: loading,
+        i18n: i18n,
+        loadAllFiles: false,
+      }),
+      h(FooterActions, {
+        partialTree: partialTree,
+        donePicking: this.donePicking,
+        cancelSelection: this.cancelSelection,
+        i18n: i18n,
+        validateAggregateRestrictions: this.validateAggregateRestrictions,
+      }),
+    );
   }
 }
-function _updateFilesAndInputMode2(res, files) {
-  this.nextPageQuery = res.nextPageQuery;
-  res.items.forEach(item => {
-    files.push(item);
-  });
-  this.plugin.setPluginState({
-    currentSelection: [],
-    isInputMode: false,
-    files,
-    searchTerm: res.searchedFor,
-  });
-}
 SearchProviderView.VERSION = packageJson.version;

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
4 participants