uppy
uppy copied to clipboard
Provider views rewrite (.files, .folders => .partialTree)
fixes #5000, fixes #4609, fixes #4414, fixes #5063
Description
-
enables indeterminate checkmark states
-
enables folder caching
-
fixes the issue where Unsplash was only loading one page
-
removed two-way binding in
onFirstRender
(not a backwards-compatible change, but only for people with custom providers) -
addresses this https://github.com/transloadit/uppy/pull/4537#discussion_r1260683896,
absDirPath
andrelDirPath
are injected in a single place -
nOfSelectedFiles
is as smart as it gets now -
fixes the UI issue where shift-clicking files gets them highlighted:
-
fixes the way shift-clicking works in grid providers such as Instagram/Unpslash
-
makes the GoogleDrive's VIRTUAL_SHARED_DIR checkable (see this discussion https://transloadit.slack.com/archives/C0FMW9PSB/p1714529071856209)
TODO
- In other PRs & when reviewers agree on the workding, translate "
You selected %{size} of files, but maximum allowed size is %{sizeAllowed
" to other locales. - To Evgenia - don't forget to remove
loadAllFiles: false
&limit: 5
from providers when preparing for a review - To Evgenia - don't forget to remove
console.log
s
Future TODOs
- I don't like how restrictions are handled currently. I think individual-file restrictions should disable the checkboxes (like they do now); but aggregate restrictions should only be shown in the footer. As a benefit, this can be made consistent with what it looks like when we're dropping files from our local file system.
Notes to reviewers
- I made deliberate effort not to touch the folder structure at all (for ease of reviewing & because we didn't set our minds on which one we'd prefer yet)
- This PR actually reduces the number of lines by a few hundred lines - the increase is due to the test file I added
Once merge conflicts are resolved we can give this an early review I think
Once merge conflicts are resolved we can give this an early review I think
Nope not a very good idea, it's in flux. I'll tell you when it's ready for a review.
@nqst, as per our call - I will describe my questions about restrictions UI. I think this is best done as a series of situations.
Situation 1
const restrictions = { maxNumberOfFiles: 3 }
I check a folder that has 4 files in it, but we don't yet know about it.
I open that folder, and we find out that it has 4 files inside!
What should happen here.
My ideas:
- It shows those 4 files as selected, but in the footer we show "Uppy only allows 3 files, but 4 files were selected"
- it stills allows us to click the
SELECT (4)
button, it will tell us to reduce the number of files in the next screen [explanation: in the next screen we will actually be seeing all files we have ever added, and we will see file sizes - it will be easier for the user to select which files are excessive] - OR it doesn't allow us to click the
SELECT (4)
- it stills allows us to click the
- It doesn't show those 4 files as selected - it only selects the first 3 files. The issue with this option is - what should we do with the parent folder, should we update it to "partially checked" folder state?
- It shows those 4 files as selected, but, when we click
SELECT (4)
, we get some error notification
Situation 2
const restrictions = { maxNumberOfFiles: 2000 }
I check a folder, which has thousands of files in it. I don't yet know how many - we will discover it upon scrolling.
I open it, and all files are marked as checked.
As I keep scrolling, we stumple upon the 2000ths file. What should happen next? Should the 2001st file NOT get marked as checked? Again, what should happen to the parent folder - should it update its state to "partially checked"?
Situation 3
const restrictions = { maxNumberOfFiles: 3 }
I check some folders. I never open any of these folders, and we do not know how many files there are.
I click "Select (2)". There are 1000 files inside of those folders, which violates our restrictions. What should happen?
My ideas:
- Only add the first 3 files we fetch, and show the notification to the user "Not adding 997 files because they didn't pass restrictions"
- Do NOT add any files - show user the error in the footer, saying "Uppy only allows 3 files, but 4 files were selected"
- Do add ALL 1000 files without any warnings - and show the "Uppy only allows 3 files, but 1000 files were selected" error in the footer of the next page.
Situation 4
const restrictions = { maxNumberOfFiles: 3 }
I check 1 file.
and then I shift-click to the 5th file
What should happen?
Situation 5
const restrictions = { maxNumberOfFiles: 3 }
I check 3 files.
What should happen, should other checkboxes grey out like they currently do?
Or, if we add the error message in the footer - should we let the user select the 4th file and only then show them the error?
How to play with it locally
You can checkout this branch to play with it if it helps! I set GoogleDrive's page limit to 10 files so that it's easy to test both the scrolling behaviour and opening the folder behaviour.
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. And if we disable this, we'll make downloading large number of files outright impossible.
Also - I regularly use folder selection in the wild myself, for example when you're uploading your assignment on university websites, they expect a particular folder structure to be kept.
Sounds like it's getting a bit complicated now. Maybe for simplicity of implementation we could:
- Allow the user to check however many files/folder they want without any restrictions in the UI
- Once the user clicks "select (X) files", then we recurse through all selected directories and count all files. Once we exceed the limit, the we do NOT add any files, but instead show an error message "Uppy only allows X files, but Y files were selected". then the user can go back and deselect files/folders.
First want to say that thanks for putting in big effort of cleaning this up AND writing a lot tests ✨
If we want to keep folder selection, I would follow these options
Situation 1: max files set to 3, you select a folder & then open it, it has 4 files. --> It shows those 4 files as selected, but, when we click SELECT (4), we get some error notification
Situation 2: identical to situation one from my understanding.
Situation 3: I click "Select (2)". There are 1000 files inside of those folders, which violates our restrictions. --> I think once again the same? If you open them one by one, you'll see "select {number}" but once you click it none are added and you see an error
Situation 5: should checkboxes be disabled once you reach the limit. --> I don't think we have to to simplify it + we kind of have to, as we allow selecting beyond the limit in the other examples.
I do think it's essential however to break this PR up into smaller PRs. I understand this made sense to experiment and find a direction, but once we have that I expect this to be four PRs or so. For instance:
- shift click fixes
- removing the
View
class - Adding basic
partialTree
structure - refactor of
ProviderView
/SearchProviderView
.
It will be too hard to review otherwise I'm afraid.
@mifi Sounds like it's getting a bit complicated now. Maybe for simplicity of implementation we could:
- Allow the user to check however many files/folder they want without any restrictions in the UI
- Once the user clicks "select (X) files", then we recurse through all selected directories and count all files. Once we exceed the limit, the we do NOT add any files, but instead show an error message "Uppy only allows X files, but Y files were selected". then the user can go back and deselect files/folders.
I like this idea, however consider the following situation.
Currently, when we have const restrictions = { maxNumberOfFiles: 1 }
and we check a single file, all the other files become unselectable:
If we implement "restriction validation only on Select (x)", the user will have no way to discover whether they have selected too many files - imagine having selected 10 files only to discover 3 files were allowed. This is especially important for the maxTotalFileSize
restriction, because file sizes are harder to predict. Also - I believe const restrictions = { maxNumberOfFiles: 1 }
is a pretty frequent use case, and all the files greying out upon the selection of a single file is attractive.
I agree it's the simplest implementation possible, in fact what you're suggesting is exactly what I decided to do a week or so ago in order to "ship this PR, and maybe think through better restrictions system in further PRs".
But I think the situation I just described is a serious downside, so I decided to ask for Alex's help in coming up with the "ideal restrictions system". Once we know what an "ideal restrictions system" would look like, it will be easier to see what I must implement in this PR, and what we can leave to others.
One variant of what you're suggesting is "allow the user to check however many files/folders they want without any restrictions in the UI; but show the error in the footer next to Select (x) if they violate some restrictions".
This solves the issue I just described; but does remove the attractive greying-out of the checkboxes (we will only show the error when the user checks 2 files, we won't see any feedback when the user checks 1 file).
I agree we should keep it simple. I don't think we should be engineering an "ideal" system right now. We could think about some magic solution(s), but it seems it's going to be prone to bugs and always have edge cases which does more harm than good both for us and the users.
So, I mostly agree with Merlijn's suggested solutions and Mikael's idea to:
Allow the user to check however many files/folder they want without any restrictions in the UI
It's not a significant issue to allow users to select files and then show an error if restrictions aren't respected. However, I have some ideas that can hopefully make the entire experience smoother:
-
We could display the restriction message when the user is selecting files as well. The initial message about restrictions on the home screen can be easily ignored, but having the message visible in the file browser may help prevent incorrect actions.
-
I like how nicely we currently support the case when the user can select only one file, and I agree that would be good to keep this. My idea is to keep this behavior only for
{ maxNumberOfFiles: 1 }
, similar to a regular<input type="file">
without themultiple
attribute set. Therefore, if the widely-used single-file mode is on, we'll disable the rest of the files when one file is selected. But if a more advanced restriction mode is enabled, let's not over-engineer it — allow users to select what they want, and then show an error if something isn't right. -
The error message should be more clear. When testing, I saw this:
It doesn't look good to me. The messages contradict each other, and the error doesn't look like an error. It would be great to improve this.
What do you folks think?
One variant of what you're suggesting is "allow the user to check however many files/folders they want without any restrictions in the UI; but show the error in the footer next to Select (x) if they violate some restrictions".
This solves the issue I just described; but does remove the attractive greying-out of the checkboxes (we will only show the error when the user checks 2 files, we won't see any feedback when the user checks 1 file).
I agree. Maybe we could even grey out checkboxes if the user has reached the limit. It will not be 100% correct (because the user might have selected at least one folder which contains more files), however it's an optimistic guess. The only problem I see (with greying out optimistically based on number of checked checkboxes) is that if the user checks a folder, but later it turns out the folder is empty, then the folder was counted as 1 file towards the limit, but in reality there are no files inside, so the user could actually have added 1 more file. Or maybe we should count checked folders as 0 towards the limit.
@nqst,
- Making this work for
{ maxNumberOfFiles: 1 }
is not significantly easier than making it work for{ maxNumberOfFiles: 200 }
. We get into the same "5 situations" I described above. - Agreed error messages have to be reworked.
@nqst: I agree we should keep it simple. I don't think we should be engineering an "ideal" system right now. We could think about some magic solution(s), but it seems it's going to be prone to bugs and always have edge cases which does more harm than good both for us and the users.
"Ideal" system doesn't necessarily bring complexity in implementation with it! We don't have to think it through now however, I'm friendly towards the idea of implementing a bare minimum solution for providers in this PR, and drawing upon this blank slate later, after we decide on a better restrictions system.
Let's think about what would be the minimal restrictions system in this PR that we are willing to accept. The easiest thing to implement would indeed be @Murderlon and @mifi -suggested "we pretend there are no aggregate restrictions" solution.
I will describe to you 2 solutions that are equally easy to implement. In both of these solutions we are not greying out files (to be more precise - we are greying out files with individual restrictions, but not files with aggregate restrictions).
Solution 1: error on click
const restrictions = { maxNumberOfFiles: 3 }
- User checks as many files as they like, there is no feedback telling them they are doing something wrong. User checks 5 files.
- User clicks "Select (X)"
- User sees the notification "Please select at most 3 files"
- User is still in the GoogleDrive interface - they are free to uncheck some files and try clicking "Select (X)" again.
Solution 2: error in the footer
const restrictions = { maxNumberOfFiles: 3 }
-
User checks as many files as they like. User checks 5 files.
-
In the footer, they see the "You can only select 3 files" error. The "Select (X)" button is disabled.
-
User unchecks 2 files. Error disappears.
@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 👍
Making this work for
{ maxNumberOfFiles: 1 }
is not significantly easier than making it work for{ maxNumberOfFiles: 200 }
. We get into the same "5 situations" I described above.
I think in the case of { maxNumberOfFiles: 1 }
, we could apply Merlijn's idea to remove the folder selection. This change would immediately resolve situations 1-4, and make the UI clearer. As for situation 5, we can gray out remaining files after one file has been selected.
This means that the one-file-only scenario would have its own distinct behavior, but I believe this could be beneficial and less confusing for users.
What do you think?
@nqst, I think if we have the "error in the footer", we don't need to disable folders to accompany the greying-out; "error in the footer" already deals with the 5 situations I described.
So, now we have the following choice.
Solution 1
- show the error in the footer
- DON'T grey out anything
Solution 2
- show the error in the footer
-
additionally grey out files if we have
{ maxNumberOfFiles: 1 }
Solution 3
- show the error in the footer
-
additionally grey out files if we have
{ maxNumberOfFiles: any }
or{ maxTotalFileSize: any }
Thing is - Solution 2 and Solution 3 are about equal in difficulty/overengineering, for both we need custom handling. So, I would either go for Solution 1, or for Solution 3.
I think Solution 3 is already close to one variant of what I described as "ideal restriction systems". But I feel we are locking ourselves into this option instead of thinking through the alternative "ideal restriction systems". One alternative would be what I think I described to you in our call - showing users the error in the footer; but letting them proceed with the selection, so that they can remove excessive files in the upload view.
I think that unless you think Solution 3 is a good option long-term, I should just implement Solution 1 in this PR, and leave further thinking on this topic to other PRs.
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.
When it's done, tho, I propose testing the one-file scenario and exploring nicer UI possibilities for this case in the future.
Agreed.
I do like the idea of disabling folder selection for { maxNumberOfFiles: 1 }
as an additional prettification of ui, let's think that through in further PRs.
Thank you all for your input @nqst, @Murderlon, @mifi! I think we settled on a solid choice, I will invite you all for a review when the PR is ready 👍
Diff output files
diff --git a/packages/@uppy/core/lib/Uppy.js b/packages/@uppy/core/lib/Uppy.js
index 3187de9..dd04229 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..b5c4041 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,
+ currentFolderId,
+ } = this.getPluginState();
+ const foldersInThisFolder = partialTree.filter(i => i.type === "folder" && i.parentId === currentFolderId);
+ if (foldersInThisFolder.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/DriveProviderViews.js b/packages/@uppy/google-drive/lib/DriveProviderViews.js
index 4ac63e3..3f73bf9 100644
--- a/packages/@uppy/google-drive/lib/DriveProviderViews.js
+++ b/packages/@uppy/google-drive/lib/DriveProviderViews.js
@@ -1,10 +1,9 @@
import { ProviderViews } from "@uppy/provider-views";
export default class DriveProviderViews extends ProviderViews {
- toggleCheckbox(e, file) {
- e.stopPropagation();
- e.preventDefault();
- if (!file.custom.isSharedDrive) {
- super.toggleCheckbox(e, file);
+ toggleCheckbox(item, isShiftKeyPressed) {
+ var _item$data$custom;
+ if (!((_item$data$custom = item.data.custom) != null && _item$data$custom.isSharedDrive)) {
+ super.toggleCheckbox(item, isShiftKeyPressed);
}
}
}
diff --git a/packages/@uppy/google-photos/lib/GooglePhotos.js b/packages/@uppy/google-photos/lib/GooglePhotos.js
index 3efe1c0..fd558ec 100644
--- a/packages/@uppy/google-photos/lib/GooglePhotos.js
+++ b/packages/@uppy/google-photos/lib/GooglePhotos.js
@@ -89,7 +89,12 @@ export default class GooglePhotos extends UIPlugin {
this.unmount();
}
render(state) {
- if (this.getPluginState().files.length && !this.getPluginState().folders.length) {
+ const {
+ partialTree,
+ currentFolderId,
+ } = this.getPluginState();
+ const foldersInThisFolder = partialTree.filter(i => i.type === "folder" && i.parentId === currentFolderId);
+ if (foldersInThisFolder.length === 0) {
return this.view.render(state, {
viewType: "grid",
showFilter: false,
diff --git a/packages/@uppy/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 06876c0..2b692ca 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: () => viewType === "grid" && f.thumbnail ? f.thumbnail : 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,
virtualList,
} = 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 (virtualList) {
+ 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 (virtualList) {
- 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..57ee6ec 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 getNumberOfSelectedFiles from "./utils/PartialTreeUtils/getNumberOfSelectedFiles.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 getNumberOfSelectedFiles(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/GridItem.js b/packages/@uppy/provider-views/lib/Item/components/GridItem.js
index d91bab6..9b425f2 100644
--- a/packages/@uppy/provider-views/lib/Item/components/GridItem.js
+++ b/packages/@uppy/provider-views/lib/Item/components/GridItem.js
@@ -1,54 +1,42 @@
-import classNames from "classnames";
import { h } from "preact";
-function GridItem(props) {
- const {
+import ItemIcon from "./ItemIcon.js";
+function GridItem(_ref) {
+ let {
+ file,
+ toggleCheckbox,
className,
isDisabled,
restrictionError,
- isChecked,
- title,
- itemIconEl,
showTitles,
- toggleCheckbox,
- recordShiftKeyPress,
- id,
- children,
- } = props;
- const checkBoxClassName = classNames(
- "uppy-u-reset",
- "uppy-ProviderBrowserItem-checkbox",
- "uppy-ProviderBrowserItem-checkbox--grid",
- {
- "uppy-ProviderBrowserItem-checkbox--is-checked": isChecked,
- },
- );
+ children = null,
+ } = _ref;
return h(
"li",
{
className: className,
- title: isDisabled ? restrictionError == null ? void 0 : restrictionError.message : undefined,
+ title: isDisabled && restrictionError ? restrictionError : undefined,
},
h("input", {
type: "checkbox",
- className: checkBoxClassName,
+ className: "uppy-u-reset uppy-ProviderBrowserItem-checkbox uppy-ProviderBrowserItem-checkbox--grid",
onChange: toggleCheckbox,
- onKeyDown: recordShiftKeyPress,
- onMouseDown: recordShiftKeyPress,
name: "listitem",
- id: id,
- checked: isChecked,
+ id: file.id,
+ checked: file.status === "checked",
disabled: isDisabled,
"data-uppy-super-focusable": true,
}),
h(
"label",
{
- htmlFor: id,
- "aria-label": title,
+ htmlFor: file.id,
+ "aria-label": file.data.name,
className: "uppy-u-reset uppy-ProviderBrowserItem-inner",
},
- itemIconEl,
- showTitles && title,
+ h(ItemIcon, {
+ itemIconString: file.data.thumbnail || file.data.icon,
+ }),
+ showTitles && file.data.name,
children,
),
);
diff --git a/packages/@uppy/provider-views/lib/Item/components/ListItem.js b/packages/@uppy/provider-views/lib/Item/components/ListItem.js
index 7e79d2c..7417bac 100644
--- a/packages/@uppy/provider-views/lib/Item/components/ListItem.js
+++ b/packages/@uppy/provider-views/lib/Item/components/ListItem.js
@@ -1,72 +1,75 @@
import { h } from "preact";
-export default function ListItem(props) {
- const {
+import ItemIcon from "./ItemIcon.js";
+export default function ListItem(_ref) {
+ let {
+ file,
+ openFolder,
className,
isDisabled,
restrictionError,
- isCheckboxDisabled,
- isChecked,
toggleCheckbox,
- recordShiftKeyPress,
- type,
- id,
- itemIconEl,
- title,
- handleFolderClick,
showTitles,
i18n,
- } = props;
+ } = _ref;
return h(
"li",
{
className: className,
- title: isDisabled ? restrictionError == null ? void 0 : restrictionError.message : undefined,
+ title: file.status !== "checked" && restrictionError ? restrictionError : undefined,
},
- !isCheckboxDisabled
- ? h("input", {
- type: "checkbox",
- className: `uppy-u-reset uppy-ProviderBrowserItem-checkbox ${
- isChecked ? "uppy-ProviderBrowserItem-checkbox--is-checked" : ""
- }`,
- onChange: toggleCheckbox,
- onKeyDown: recordShiftKeyPress,
- onMouseDown: recordShiftKeyPress,
- name: "listitem",
- id: id,
- checked: isChecked,
- "aria-label": type === "file" ? null : i18n("allFilesFromFolderNamed", {
- name: title,
- }),
- disabled: isDisabled,
- "data-uppy-super-focusable": true,
- })
- : null,
- type === "file"
+ h("input", {
+ type: "checkbox",
+ className: "uppy-u-reset uppy-ProviderBrowserItem-checkbox",
+ onChange: toggleCheckbox,
+ name: "listitem",
+ id: file.id,
+ checked: file.status === "checked",
+ "aria-label": file.data.isFolder
+ ? i18n("allFilesFromFolderNamed", {
+ name: file.data.name,
+ })
+ : null,
+ disabled: isDisabled,
+ "data-uppy-super-focusable": true,
+ }),
+ file.data.isFolder
? h(
- "label",
- {
- htmlFor: id,
- className: "uppy-u-reset uppy-ProviderBrowserItem-inner",
- },
- h("div", {
- className: "uppy-ProviderBrowserItem-iconWrap",
- }, itemIconEl),
- showTitles && title,
- )
- : h(
"button",
{
type: "button",
className: "uppy-u-reset uppy-c-btn uppy-ProviderBrowserItem-inner",
- onClick: handleFolderClick,
+ onClick: () => openFolder(file.id),
"aria-label": i18n("openFolderNamed", {
- name: title,
+ name: file.data.name,
+ }),
+ },
+ h(
+ "div",
+ {
+ className: "uppy-ProviderBrowserItem-iconWrap",
+ },
+ h(ItemIcon, {
+ itemIconString: file.data.icon,
}),
+ ),
+ showTitles && file.data.name ? h("span", null, file.data.name) : i18n("unnamed"),
+ )
+ : h(
+ "label",
+ {
+ htmlFor: file.id,
+ className: "uppy-u-reset uppy-ProviderBrowserItem-inner",
},
- h("div", {
- className: "uppy-ProviderBrowserItem-iconWrap",
- }, itemIconEl),
- showTitles && title ? h("span", null, title) : i18n("unnamed"),
+ h(
+ "div",
+ {
+ className: "uppy-ProviderBrowserItem-iconWrap",
+ },
+ h(ItemIcon, {
+ itemIconString: file.data.icon,
+ }),
+ ),
+ showTitles && file.data.name,
),
);
}
diff --git a/packages/@uppy/provider-views/lib/Item/index.js b/packages/@uppy/provider-views/lib/Item/index.js
index be3e2a7..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 GridItem from "./components/GridItem.js";
-import ItemIcon from "./components/ItemIcon.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(
- GridItem,
- _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(
GridItem,
- _extends({}, props, {
- className: className,
- itemIconEl: itemIconEl,
- }),
+ 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 2ae6af4..66f9861 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.9",
};
-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,306 +39,261 @@ export function defaultPickerIcon() {
}),
);
}
-const defaultOptions = {
- viewType: "list",
- showTitles: true,
- showFilter: true,
- showBreadcrumbs: true,
- loadAllFiles: false,
- virtualList: 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,
+ virtualList: 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.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 {
@@ -349,91 +304,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,
- virtualList: this.opts.virtualList,
- 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,
+ virtualList: opts.virtualList,
+ 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 +390,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 +401,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 36d73fb..011fe73 100644
--- a/packages/@uppy/provider-views/lib/SearchProviderView/SearchProviderView.js
+++ b/packages/@uppy/provider-views/lib/SearchProviderView/SearchProviderView.js
@@ -1,205 +1,261 @@
-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.9",
};
+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.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,
+ buttonCSSClassName: "uppy-SearchProvider-searchButton",
+ });
}
- 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,
+ virtualList: 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;
Unfortunately the PR is overwhelmingly big, I'm having a hard time reviewing it. A lot of time went into this already and there are code comments, tests, and from a glance most things seem very well structured. So I don't think it makes sense to force you to split this up into many PRs.
Realistically the only way this could have been a set of smaller PRs is if I have been splitting this PR post-factum.
There hasn't been a way forward with a number of smaller PRs. I changed the underlying data structure multiple times as I was going through with this PR, and every such overhaul was touching most files. It's only now clear what smaller PRs could have been of use in the final version.
I can still do the post-factum split, but I don't believe it will significantly ease the review for you - the main workload of this PR is in changing the data structure used throughout provider views, which by its nature touches 40 files.
Sometimes (say, when the data structure used by all files changes completely) a big PR is unavoidable.
I understand it's challenging to review - like mentioned in the PR description, I suggest going through the folders in your editor, and seeing if it make sense. In particular would be great if the reviewers could go through code in /utils
and see if it compiles in their head.
Realistically the only way this could have been a set of smaller PRs is if I have been splitting this PR post-factum.
There hasn't been a way forward with a number of smaller PRs. I changed the underlying data structure multiple times as I was going through with this PR, and every such overhaul was touching most files. It's only now clear what smaller PRs could have been of use in the final version.
Yes but here's the important thing, that doesn't matter. It's about making reviewable PRs in which we follow along your thinking. Changing the data structure three times? No problem, we can review with you to see if that's the right direction. If we did that, everyone on the team would be more familiar with this code, they would understand the thinking that got us here, and perhaps even adjusted the evolution of the data structure as now they're more familiar from the previous PRs.
Instead we're presented with a massive final result of all your thoughts combined over the course of weeks. It's simply not possible to review this with the same accuracy.
In far more cases then we think, big PRs are in fact avoidable. The only problem is the process. Against my best wishes, we don't have stacked PRs on GitHub. If we did this would feel a lot more natural. But it can still be done, and I'm reemphasizing this not to make you feel bad but to suggest we can do this for future cases like this.
👍 Dependency issues cleared. Learn more about Socket for GitHub ↗︎
This PR previously contained dependency changes with security issues that have been resolved, removed, or ignored.
Tested with
- [x] GoogleDrive
- [x] GooglePhotos
- [x] Box
- [x] Dropbox
- [x] Facebook
- [x] Instagram
- [x] OneDrive
- [x] Unsplash
- [x] Zoom
The testing process consists of the following procedure:
1. sign in 2. check deeply nested files 3. download them 4. sign out
.
New dependencies detected. Learn more about Socket for GitHub ↗︎
Package | New capabilities | Transitives | Size | Publisher |
---|---|---|---|---|
npm/@uppy-dev/[email protected] | None | 0 |
0 B |