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 156 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

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 files & comments

GoogleDrive
- travelling down into folders works
- checking a file works
- breadcrumbs DONT work
@lakesare lakesare changed the base branch from main to 4.x May 31, 2024 05:59
Copy link
Contributor

github-actions bot commented May 31, 2024

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/Uppy.js b/packages/@uppy/core/lib/Uppy.js
index 67bbe14..c7670eb 100644
--- a/packages/@uppy/core/lib/Uppy.js
+++ b/packages/@uppy/core/lib/Uppy.js
@@ -452,14 +452,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/facebook/lib/Facebook.js b/packages/@uppy/facebook/lib/Facebook.js
index b8f870d..1206c29 100644
--- a/packages/@uppy/facebook/lib/Facebook.js
+++ b/packages/@uppy/facebook/lib/Facebook.js
@@ -71,13 +71,18 @@ 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,
+      });
     }
-    return this.view.render(state, viewOptions);
+    return this.view.render(state);
   }
 }
 Facebook.VERSION = packageJson.version;
diff --git a/packages/@uppy/google-drive/lib/GoogleDrive.js b/packages/@uppy/google-drive/lib/GoogleDrive.js
index cf021fc..e172dda 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.6",
@@ -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 1cff9cb..0c7087f 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..bedd14c 100644
--- a/packages/@uppy/provider-views/lib/Browser.js
+++ b/packages/@uppy/provider-views/lib/Browser.js
@@ -1,205 +1,90 @@
-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,
+        }),
+      ),
+    );
+  }
+  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..3073f7a 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.js";
 export default function FooterActions(_ref) {
   let {
-    cancel,
-    done,
+    cancelSelection,
+    donePicking,
     i18n,
-    selected,
+    partialTree,
+    validateAggregateRestrictions,
   } = _ref;
+  const aggregateRestrictionError = useMemo(() => {
+    return validateAggregateRestrictions(partialTree);
+  }, [partialTree, validateAggregateRestrictions]);
+  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/components/ItemIcon.js b/packages/@uppy/provider-views/lib/Item/components/ItemIcon.js
index f101814..aebdd51 100644
--- a/packages/@uppy/provider-views/lib/Item/components/ItemIcon.js
+++ b/packages/@uppy/provider-views/lib/Item/components/ItemIcon.js
@@ -53,10 +53,11 @@ function VideoIcon() {
     }),
   );
 }
-export default function ItemIcon(props) {
-  const {
+export default function ItemIcon(_ref) {
+  let {
     itemIconString,
-  } = props;
+    alt = undefined,
+  } = _ref;
   if (itemIconString === null) return null;
   switch (itemIconString) {
     case "file":
@@ -66,9 +67,6 @@ export default function ItemIcon(props) {
     case "video":
       return h(VideoIcon, null);
     default: {
-      const {
-        alt,
-      } = props;
       return h("img", {
         src: itemIconString,
         alt: alt,
diff --git a/packages/@uppy/provider-views/lib/Item/index.js b/packages/@uppy/provider-views/lib/Item/index.js
index 52ee30c..dc2c3ba 100644
--- a/packages/@uppy/provider-views/lib/Item/index.js
+++ b/packages/@uppy/provider-views/lib/Item/index.js
@@ -1,68 +1,53 @@
-function _extends() {
-  return _extends = Object.assign ? Object.assign.bind() : function(n) {
-    for (var e = 1; e < arguments.length; e++) {
-      var t = arguments[e];
-      for (var r in t) ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]);
-    }
-    return n;
-  },
-    _extends.apply(null, arguments);
-}
 import classNames from "classnames";
 import { h } from "preact";
-import GridListItem from "./components/GridLi.js";
-import ItemIcon from "./components/ItemIcon.js";
-import ListItem from "./components/ListLi.js";
+import GridItem from "./components/GridItem.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 = !!restrictionError && file.status !== "checked";
+  const ourProps = {
+    file,
+    openFolder,
+    toggleCheckbox,
+    i18n,
+    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",
+    }),
+    isDisabled,
+    restrictionError,
+  };
   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/AuthView.js b/packages/@uppy/provider-views/lib/ProviderView/AuthView.js
index 3902d4f..8e23845 100644
--- a/packages/@uppy/provider-views/lib/ProviderView/AuthView.js
+++ b/packages/@uppy/provider-views/lib/ProviderView/AuthView.js
@@ -99,15 +99,15 @@ const defaultRenderForm = _ref2 => {
     onAuth: onAuth,
   });
 };
-export default function AuthView(props) {
-  const {
+export default function AuthView(_ref3) {
+  let {
     loading,
     pluginName,
     pluginIcon,
     i18n,
     handleAuth,
     renderForm = defaultRenderForm,
-  } = props;
+  } = _ref3;
   return h(
     "div",
     {
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 e0f3d71..fc0d510 100644
--- a/packages/@uppy/provider-views/lib/ProviderView/ProviderView.js
+++ b/packages/@uppy/provider-views/lib/ProviderView/ProviderView.js
@@ -6,24 +6,24 @@ var id = 0;
 function _classPrivateFieldLooseKey(e) {
   return "__private_" + id++ + "_" + e;
 }
-import { getSafeFileId } from "@uppy/utils/lib/generateFileID";
-import PQueue from "p-queue";
+import remoteFileObjToLocal from "@uppy/utils/lib/remoteFileObjToLocal";
+import classNames from "classnames";
 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.7",
 };
-function formatBreadcrumbs(breadcrumbs) {
-  return breadcrumbs.slice(1).map(directory => directory.name).join("/");
-}
-function prependPath(path, component) {
-  if (!path) return component;
-  return `${path}/${component}`;
-}
+import FooterActions from "../FooterActions.js";
+import SearchInput from "../SearchInput.js";
+import addFiles from "../utils/addFiles.js";
+import getClickedRange from "../utils/getClickedRange.js";
+import handleError from "../utils/handleError.js";
+import getBreadcrumbs from "../utils/PartialTreeUtils/getBreadcrumbs.js";
+import getCheckedFilesWithPaths from "../utils/PartialTreeUtils/getCheckedFilesWithPaths.js";
+import PartialTreeUtils from "../utils/PartialTreeUtils/index.js";
+import shouldHandleScroll from "../utils/shouldHandleScroll.js";
 export function defaultPickerIcon() {
   return h(
     "svg",
@@ -39,305 +39,260 @@ 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.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;
+    const {
+      partialTree,
+    } = this.plugin.getPluginState();
+    const clickedFolder = partialTree.find(folder => folder.id === folderId);
+    if (clickedFolder.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 {
@@ -348,90 +303,82 @@ 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,
       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));
+    const {
+      partialTree,
+      currentFolderId,
+      username,
+      searchString,
+    } = this.plugin.getPluginState();
+    const breadcrumbs = getBreadcrumbs(partialTree, currentFolderId);
+    return h(
+      "div",
+      {
+        className: classNames("uppy-ProviderBrowser", `uppy-ProviderBrowser-viewType--${opts.viewType}`),
+      },
+      h(Header, {
+        showBreadcrumbs: opts.showBreadcrumbs,
+        openFolder: this.openFolder,
+        breadcrumbs: breadcrumbs,
+        pluginIcon: pluginIcon,
+        title: this.plugin.title,
+        logout: this.logout,
+        username: username,
+        i18n: i18n,
+      }),
+      opts.showFilter && h(SearchInput, {
+        searchString: searchString,
+        setSearchString: s => {
+          this.plugin.setPluginState({
+            searchString: s,
+          });
+        },
+        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) {
@@ -442,7 +389,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);
@@ -454,90 +400,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/SearchProviderView/SearchProviderView.js b/packages/@uppy/provider-views/lib/SearchProviderView/SearchProviderView.js
index 07f1ca1..55fc3c7 100644
--- a/packages/@uppy/provider-views/lib/SearchProviderView/SearchProviderView.js
+++ b/packages/@uppy/provider-views/lib/SearchProviderView/SearchProviderView.js
@@ -1,205 +1,260 @@
-function _classPrivateFieldLooseBase(e, t) {
-  if (!{}.hasOwnProperty.call(e, t)) throw new TypeError("attempted to use private field on non-instance");
-  return e;
-}
-var id = 0;
-function _classPrivateFieldLooseKey(e) {
-  return "__private_" + id++ + "_" + e;
-}
+import remoteFileObjToLocal from "@uppy/utils/lib/remoteFileObjToLocal";
+import classNames from "classnames";
 import { h } from "preact";
 import Browser from "../Browser.js";
-import CloseWrapper from "../CloseWrapper.js";
-import SearchFilterInput from "../SearchFilterInput.js";
-import View from "../View.js";
+import SearchInput from "../SearchInput.js";
 const packageJson = {
   "version": "4.0.0-beta.7",
 };
+import FooterActions from "../FooterActions.js";
+import addFiles from "../utils/addFiles.js";
+import getClickedRange from "../utils/getClickedRange.js";
+import handleError from "../utils/handleError.js";
+import getCheckedFilesWithPaths from "../utils/PartialTreeUtils/getCheckedFilesWithPaths.js";
+import PartialTreeUtils from "../utils/PartialTreeUtils/index.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(SearchInput, {
+        searchString: searchString,
+        setSearchString: this.setSearchString,
+        submitSearchString: this.search,
+        inputLabel: i18n("enterTextToSearch"),
+        buttonLabel: i18n("searchImages"),
+        wrapperClassName: "uppy-SearchProvider",
+        inputClassName: "uppy-c-textInput uppy-SearchProvider-input",
+        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(SearchInput, {
+        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;

This comment was marked as off-topic.

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