-
Notifications
You must be signed in to change notification settings - Fork 1.9k
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
base: 4.x
Are you sure you want to change the base?
Conversation
GoogleDrive - travelling down into folders works - checking a file works - breadcrumbs DONT work
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. |
@Murderlon, disagreed, people clearly use folder selection, see all the discussions about relativePath vs absolutePath. 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. |
Sounds like it's getting a bit complicated now. Maybe for simplicity of implementation we could:
|
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. 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. Situation 5: should checkboxes be disabled once you reach the limit. 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:
It will be too hard to review otherwise I'm afraid. |
I like this idea, however consider the following situation. 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 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". 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 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:
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:
What do you folks think? |
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. |
|
"Ideal" system doesn't necessarily bring complexity in implementation with it! Let's think about what would be the minimal restrictions system in this PR that we are willing to accept. I will describe to you 2 solutions that are equally easy to implement. Solution 1: error on click
Solution 2: error in the footer
@nqst, @mifi, @Murderlon - do you find either of these solutions acceptable, and do you have a preference for one over the other? |
if we are not greying out checkboxes once the limit has been reached, then i like solution 2 more |
Option 2 sounds good! |
@lakesare I also like the second solution you proposed 👍
I think in the case of 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? |
@nqst, So, now we have the following choice. Solution 1
Solution 2
Solution 3
Thing is - Solution 2 and Solution 3 are about equal in difficulty/overengineering, for both we need custom handling. I think Solution 3 is already close to one variant of what I described as "ideal restriction systems". 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. |
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. |
Agreed. Thank you all for your input @nqst, @Murderlon, @mifi! |
Same notifications, same code, same everything
…port aggregate error
Diff output filesdiff --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; |
Description
enables indeterminate checkmark states
[fixes Nested folder selection does not work correctly #4609]
enables folder caching
fixes the issue where Unsplash was only loading one page
[fixes Unsplash doesn't load further pages #5000]
reworks restrictions system
[fixes Fix logic with adding folders toast message #4414]
removes two-way binding in
onFirstRender
(not a backwards-compatible change, but only for people with custom providers)addresses this Remote file paths #4537 (comment),
absDirPath
andrelDirPath
are injected in a single placenOfSelectedFiles
is as smart as it gets nowfixes the UI issue where shift-clicking files gets them highlighted:
fixes the way shift-clicking works in grid providers such as Instagram/Unpslash
[fixes Shift-clicking works chaotically with Instagram/Unsplash #5063]
makes the GoogleDrive's VIRTUAL_SHARED_DIR checkable (see this discussion https://transloadit.slack.com/archives/C0FMW9PSB/p1714529071856209)
TODO
loadAllFiles: false
&limit: 5
from providers when preparing for a reviewconsole.log
sNotes to reviewers