Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

@uppy/provider-views

Package Overview
Dependencies
Maintainers
6
Versions
109
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@uppy/provider-views - npm Package Compare versions

Comparing version 3.1.0 to 3.2.0

lib/SearchFilterInput.js

8

CHANGELOG.md
# @uppy/provider-views
## 3.2.0
Released: 2023-04-04
Included in: Uppy v3.7.0
- @uppy/provider-views: fix race condition when adding folders (Mikael Finstad / #4384)
- @uppy/provider-views: UI: Use form attribite with a form in doc root to prevent outer form submit (Artur Paikin / #4283)
## 3.0.2

@@ -4,0 +12,0 @@

42

lib/Browser.js
import { h } from 'preact';
import classNames from 'classnames';
import remoteFileObjToLocal from '@uppy/utils/lib/remoteFileObjToLocal';
import Filter from "./Filter.js";
import SearchFilterInput from "./SearchFilterInput.js";
import FooterActions from "./FooterActions.js";

@@ -25,9 +25,15 @@ import Item from "./Item/index.js";

validateRestrictions,
showFilter,
filterQuery,
filterInput,
isLoading,
showSearchFilter,
search,
searchTerm,
clearSearch,
searchOnInput,
searchInputLabel,
clearSearchLabel,
getNextFolder,
cancel,
done,
columns
columns,
noResultsLabel
} = props;

@@ -37,15 +43,27 @@ const selected = currentSelection.length;

className: classNames('uppy-ProviderBrowser', `uppy-ProviderBrowser-viewType--${viewType}`)
}, h("div", {
}, headerComponent && h("div", {
className: "uppy-ProviderBrowser-header"
}, h("div", {
className: classNames('uppy-ProviderBrowser-headerBar', !showBreadcrumbs && 'uppy-ProviderBrowser-headerBar--simple')
}, headerComponent)), showFilter && h(Filter, {
i18n: i18n,
filterQuery: filterQuery,
filterInput: filterInput
}), (() => {
}, headerComponent)), showSearchFilter && h("div", {
class: "uppy-ProviderBrowser-searchFilter"
}, 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, i18n('loading')));
}
if (!folders.length && !files.length) {
return h("div", {
className: "uppy-Provider-empty"
}, i18n('noFilesFound'));
}, noResultsLabel);
}

@@ -52,0 +70,0 @@

import { h } from 'preact';
import classNames from 'classnames';

@@ -17,2 +18,5 @@ function GridListItem(props) {

} = props;
const checkBoxClassName = classNames('uppy-u-reset', 'uppy-ProviderBrowserItem-checkbox', 'uppy-ProviderBrowserItem-checkbox--grid', {
'uppy-ProviderBrowserItem-checkbox--is-checked': isChecked
});
return h("li", {

@@ -23,3 +27,3 @@ className: className,

type: "checkbox",
className: `uppy-u-reset uppy-ProviderBrowserItem-checkbox ${isChecked ? 'uppy-ProviderBrowserItem-checkbox--is-checked' : ''} uppy-ProviderBrowserItem-checkbox--grid`,
className: checkBoxClassName,
onChange: toggleCheckbox,

@@ -36,7 +40,5 @@ onKeyDown: recordShiftKeyPress,

className: "uppy-u-reset uppy-ProviderBrowserItem-inner"
}, h("span", {
className: "uppy-ProviderBrowserItem-inner-relative"
}, itemIconEl, showTitles && title, children)));
}, itemIconEl, showTitles && title, children));
}
export default GridListItem;

@@ -53,3 +53,4 @@ 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); }

rel: "noopener noreferrer",
className: "uppy-ProviderBrowserItem-author"
className: "uppy-ProviderBrowserItem-author",
tabIndex: "-1"
}, author.name))

@@ -56,0 +57,0 @@ );

@@ -8,2 +8,3 @@ function _classPrivateFieldLooseBase(receiver, privateKey) { if (!Object.prototype.hasOwnProperty.call(receiver, privateKey)) { throw new TypeError("attempted to use private field on non-instance"); } return receiver; }

import { h } from 'preact';
import { getSafeFileId } from '@uppy/utils/lib/generateFileID';
import AuthView from "./AuthView.js";

@@ -16,3 +17,3 @@ import Header from "./Header.js";

const packageJson = {
"version": "3.1.0"
"version": "3.2.0"
};

@@ -71,2 +72,3 @@

this.filterQuery = this.filterQuery.bind(this);
this.clearFilter = this.clearFilter.bind(this);
this.getFolder = this.getFolder.bind(this);

@@ -77,3 +79,2 @@ this.getNextFolder = this.getNextFolder.bind(this);

this.handleScroll = this.handleScroll.bind(this);
this.listAllFiles = this.listAllFiles.bind(this);
this.donePicking = this.donePicking.bind(this); // Visual

@@ -104,4 +105,7 @@

*/
getFolder(id, name) {
return this.sharedHandler.loaderWrapper(this.provider.list(id), res => {
async getFolder(id, name) {
this.setLoading(true);
try {
const res = await this.provider.list(id);
const folders = [];

@@ -130,3 +134,7 @@ const files = [];

});
}, this.handleError);
} catch (err) {
this.handleError(err);
} finally {
this.setLoading(false);
}
}

@@ -172,85 +180,11 @@ /**

filterQuery(e) {
const state = this.plugin.getPluginState();
this.plugin.setPluginState({ ...state,
filterInput: e ? e.target.value : ''
filterQuery(input) {
this.plugin.setPluginState({
filterInput: input
});
}
/**
* Adds all files found inside of specified folder.
*
* Uses separated state while folder contents are being fetched and
* mantains list of selected folders, which are separated from files.
*/
addFolder(folder) {
const folderId = this.providerFileToId(folder);
const folders = { ...this.plugin.getPluginState().selectedFolders
};
if (folderId in folders && folders[folderId].loading) {
return;
}
folders[folderId] = {
loading: true,
files: []
};
clearFilter() {
this.plugin.setPluginState({
selectedFolders: { ...folders
}
}); // eslint-disable-next-line consistent-return
return this.listAllFiles(folder.requestPath).then(files => {
let count = 0; // If the same folder is added again, we don't want to send
// X amount of duplicate file notifications, we want to say
// the folder was already added. This checks if all files are duplicate,
// if that's the case, we don't add the files.
files.forEach(file => {
const id = this.providerFileToId(file);
if (!this.plugin.uppy.checkIfFileAlreadyExists(id)) {
count++;
}
});
if (count > 0) {
files.forEach(file => this.addFile(file));
}
const ids = files.map(this.providerFileToId);
folders[folderId] = {
loading: false,
files: ids
};
this.plugin.setPluginState({
selectedFolders: folders,
filterInput: ''
});
let message;
if (count === 0) {
message = this.plugin.uppy.i18n('folderAlreadyAdded', {
folder: folder.name
});
} else if (files.length) {
message = this.plugin.uppy.i18n('folderAdded', {
smart_count: count,
folder: folder.name
});
} else {
message = this.plugin.uppy.i18n('emptyFolderAdded');
}
this.plugin.uppy.info(message);
}).catch(e => {
const selectedFolders = { ...this.plugin.getPluginState().selectedFolders
};
delete selectedFolders[folderId];
this.plugin.setPluginState({
selectedFolders
});
this.handleError(e);
filterInput: ''
});

@@ -333,40 +267,93 @@ }

async listAllFiles(path, files) {
if (files === void 0) {
files = null;
}
async *recursivelyListAllFiles(path) {
let curPath = path; // need to repeat the list call until there are no more pages
files = files || []; // eslint-disable-line no-param-reassign
while (curPath) {
const res = await this.provider.list(curPath);
const res = await this.provider.list(path);
res.items.forEach(item => {
if (!item.isFolder) {
files.push(item);
} else {
this.addFolder(item);
for (const item of res.items) {
if (item.isFolder) {
// recursively call self for folder
yield* this.recursivelyListAllFiles(item.requestPath);
} else {
yield item;
}
}
});
const moreFiles = res.nextPagePath;
if (moreFiles) {
return this.listAllFiles(moreFiles, files);
curPath = res.nextPagePath;
}
return files;
}
donePicking() {
const {
currentSelection
} = this.plugin.getPluginState();
const promises = currentSelection.map(file => {
if (file.isFolder) {
return this.addFolder(file);
}
async donePicking() {
this.setLoading(true);
return this.addFile(file);
});
this.sharedHandler.loaderWrapper(Promise.all(promises), () => {
try {
const {
currentSelection
} = this.plugin.getPluginState();
const messages = [];
const newFiles = []; // eslint-disable-next-line no-unreachable-loop
for (const file of currentSelection) {
if (file.isFolder) {
const {
requestPath,
name
} = file;
let isEmpty = true;
let numNewFiles = 0;
for await (const fileInFolder of this.recursivelyListAllFiles(requestPath)) {
const tagFile = this.getTagFile(fileInFolder);
const id = getSafeFileId(tagFile); // If the same folder is added again, we don't want to send
// X amount of duplicate file notifications, we want to say
// the folder was already added. This checks if all files are duplicate,
// if that's the case, we don't add the files.
if (!this.plugin.uppy.checkIfFileAlreadyExists(id)) {
newFiles.push(fileInFolder);
numNewFiles++;
}
isEmpty = false;
}
let message;
if (isEmpty) {
message = this.plugin.uppy.i18n('emptyFolderAdded');
} else if (numNewFiles === 0) {
message = this.plugin.uppy.i18n('folderAlreadyAdded', {
folder: name
});
} else {
message = this.plugin.uppy.i18n('folderAdded', {
smart_count: numNewFiles,
folder: name
});
}
messages.push(message);
} else {
newFiles.push(file);
}
} // Note: this.plugin.uppy.addFiles must be only run once we are done fetching all files,
// because it will cause the loading screen to disappear,
// and that will allow the user to start the upload, so we need to make sure we have
// finished all async operations before we add any file
// see https://github.com/transloadit/uppy/pull/4384
this.plugin.uppy.log('Adding remote provider files');
this.plugin.uppy.addFiles(newFiles.map(file => this.getTagFile(file)));
this.plugin.setPluginState({
filterInput: ''
});
messages.forEach(message => this.plugin.uppy.info(message));
this.clearSelection();
}, () => {});
} catch (err) {
this.handleError(err);
} finally {
this.setLoading(false);
}
}

@@ -385,2 +372,5 @@

} = this.plugin.getPluginState();
const {
i18n
} = this.plugin.uppy;

@@ -406,3 +396,3 @@ if (!didFirstRender) {

filterItems
} = this.sharedHandler;
} = this;
const hasInput = filterInput !== '';

@@ -417,3 +407,3 @@ const headerProps = {

username: this.username,
i18n: this.plugin.uppy.i18n
i18n
};

@@ -430,7 +420,13 @@ const browserProps = {

getFolder: this.getFolder,
filterItems: this.sharedHandler.filterItems,
filterQuery: this.filterQuery,
// For SearchFilterInput component
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,
listAllFiles: this.listAllFiles,
done: this.donePicking,

@@ -442,3 +438,2 @@ cancel: this.cancelPicking,

showTitles: targetViewOptions.showTitles,
showFilter: targetViewOptions.showFilter,
showBreadcrumbs: targetViewOptions.showBreadcrumbs,

@@ -445,0 +440,0 @@ pluginIcon: this.plugin.icon,

@@ -8,13 +8,12 @@ function _classPrivateFieldLooseBase(receiver, privateKey) { if (!Object.prototype.hasOwnProperty.call(receiver, privateKey)) { throw new TypeError("attempted to use private field on non-instance"); } return receiver; }

import { h } from 'preact';
import SearchInput from "./InputView.js";
import SearchFilterInput from "../SearchFilterInput.js";
import Browser from "../Browser.js";
import LoaderView from "../Loader.js";
import Header from "./Header.js";
import CloseWrapper from '../CloseWrapper.js';
import View from '../View.js';
const packageJson = {
"version": "3.1.0"
"version": "3.2.0"
};
/**
* Class to easily generate generic views for Provider plugins
* SearchProviderView, used for Unsplash and future image search providers.
* Extends generic View, shared with regular providers like Google Drive and Instagram.
*/

@@ -47,10 +46,9 @@

this.search = this.search.bind(this);
this.triggerSearchInput = this.triggerSearchInput.bind(this);
this.addFile = this.addFile.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); // Visual
this.render = this.render.bind(this); // Set default state for the plugin
this.plugin.setPluginState({
this.render = this.render.bind(this);
this.defaultState = {
isInputMode: true,

@@ -63,3 +61,5 @@ files: [],

searchTerm: null
});
}; // Set default state for the plugin
this.plugin.setPluginState(this.defaultState);
} // eslint-disable-next-line class-methods-use-this

@@ -71,12 +71,7 @@

clearSelection() {
this.plugin.setPluginState({
currentSelection: [],
isInputMode: true,
files: [],
searchTerm: null
});
resetPluginState() {
this.plugin.setPluginState(this.defaultState);
}
search(query) {
async search(query) {
const {

@@ -88,13 +83,23 @@ searchTerm

// no need to search again as this is the same as the previous search
return undefined;
return;
}
return this.sharedHandler.loaderWrapper(this.provider.search(query), res => {
this.setLoading(true);
try {
const res = await this.provider.search(query);
_classPrivateFieldLooseBase(this, _updateFilesAndInputMode)[_updateFilesAndInputMode](res, []);
}, this.handleError);
} catch (err) {
this.handleError(err);
} finally {
this.setLoading(false);
}
}
triggerSearchInput() {
clearSearch() {
this.plugin.setPluginState({
isInputMode: true
currentSelection: [],
files: [],
searchTerm: null
});

@@ -129,6 +134,5 @@ }

} = this.plugin.getPluginState();
const promises = currentSelection.map(file => this.addFile(file));
this.sharedHandler.loaderWrapper(Promise.all(promises), () => {
this.clearSelection();
}, () => {});
this.plugin.uppy.log('Adding remote search provider files');
this.plugin.uppy.addFiles(currentSelection.map(file => this.getTagFile(file)));
this.resetPluginState();
}

@@ -148,2 +152,5 @@

} = this.plugin.getPluginState();
const {
i18n
} = this.plugin.uppy;

@@ -168,3 +175,3 @@ if (!didFirstRender) {

filterItems
} = this.sharedHandler;
} = this;
const hasInput = filterInput !== '';

@@ -180,7 +187,11 @@ const browserProps = {

cancel: this.cancelPicking,
headerComponent: Header({
search: this.search,
i18n: this.plugin.uppy.i18n,
searchTerm
}),
// For SearchFilterInput component
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,

@@ -190,5 +201,6 @@ viewType: targetViewOptions.viewType,

showFilter: targetViewOptions.showFilter,
isLoading: loading,
showBreadcrumbs: targetViewOptions.showBreadcrumbs,
pluginIcon: this.plugin.icon,
i18n: this.plugin.uppy.i18n,
i18n,
uppyFiles: this.plugin.uppy.getFiles(),

@@ -200,21 +212,20 @@ validateRestrictions: function () {

if (loading) {
return h(CloseWrapper, {
onUnmount: this.clearSelection
}, h(LoaderView, {
i18n: this.plugin.uppy.i18n
}));
}
if (isInputMode) {
return h(CloseWrapper, {
onUnmount: this.clearSelection
}, h(SearchInput, {
onUnmount: this.resetPluginState
}, h("div", {
className: "uppy-SearchProvider"
}, h(SearchFilterInput, {
search: this.search,
i18n: this.plugin.uppy.i18n
}));
clearSelection: this.clearSelection,
inputLabel: i18n('enterTextToSearch'),
buttonLabel: i18n('searchImages'),
inputClassName: "uppy-c-textInput uppy-SearchProvider-input",
buttonCSSClassName: "uppy-SearchProvider-searchButton",
showButton: true
})));
}
return h(CloseWrapper, {
onUnmount: this.clearSelection
onUnmount: this.resetPluginState
}, h(Browser, browserProps));

@@ -231,2 +242,3 @@ }

this.plugin.setPluginState({
currentSelection: [],
isInputMode: false,

@@ -233,0 +245,0 @@ files,

import getFileType from '@uppy/utils/lib/getFileType';
import isPreviewSupported from '@uppy/utils/lib/isPreviewSupported';
import generateFileID from '@uppy/utils/lib/generateFileID'; // TODO: now that we have a shared `View` class,
// `SharedHandler` could be cleaned up and moved into here
import SharedHandler from './SharedHandler.js';
import remoteFileObjToLocal from '@uppy/utils/lib/remoteFileObjToLocal';
export default class View {
constructor(plugin, opts) {
this.filterItems = items => {
const state = this.plugin.getPluginState();
if (!state.filterInput || state.filterInput === '') {
return items;
}
return items.filter(folder => {
return folder.name.toLowerCase().indexOf(state.filterInput.toLowerCase()) !== -1;
});
};
this.recordShiftKeyPress = e => {
this.isShiftKeyPressed = e.shiftKey;
};
this.toggleCheckbox = (e, file) => {
e.stopPropagation();
e.preventDefault();
e.currentTarget.focus();
const {
folders,
files
} = this.plugin.getPluginState();
const items = this.filterItems(folders.concat(files)); // Shift-clicking selects a single consecutive list of items
// starting at the previous click and deselects everything else.
if (this.lastCheckbox && this.isShiftKeyPressed) {
const prevIndex = items.indexOf(this.lastCheckbox);
const currentIndex = items.indexOf(file);
const currentSelection = prevIndex < currentIndex ? items.slice(prevIndex, currentIndex + 1) : items.slice(currentIndex, prevIndex + 1);
const reducedCurrentSelection = []; // Check restrictions on each file in currentSelection,
// reduce it to only contain files that pass restrictions
for (const item of currentSelection) {
const {
uppy
} = this.plugin;
const restrictionError = uppy.validateRestrictions(remoteFileObjToLocal(item), [...uppy.getFiles(), ...reducedCurrentSelection]);
if (!restrictionError) {
reducedCurrentSelection.push(item);
} else {
uppy.info({
message: restrictionError.message
}, 'error', uppy.opts.infoTimeout);
}
}
this.plugin.setPluginState({
currentSelection: reducedCurrentSelection
});
return;
}
this.lastCheckbox = file;
const {
currentSelection
} = this.plugin.getPluginState();
if (this.isChecked(file)) {
this.plugin.setPluginState({
currentSelection: currentSelection.filter(item => item.id !== file.id)
});
} else {
this.plugin.setPluginState({
currentSelection: currentSelection.concat([file])
});
}
};
this.isChecked = file => {
const {
currentSelection
} = this.plugin.getPluginState(); // comparing id instead of the file object, because the reference to the object
// changes when we switch folders, and the file list is updated
return currentSelection.some(item => item.id === file.id);
};
this.plugin = plugin;
this.provider = opts.provider;
this.sharedHandler = new SharedHandler(plugin);
this.isHandlingScroll = false;
this.preFirstRender = this.preFirstRender.bind(this);
this.handleError = this.handleError.bind(this);
this.addFile = this.addFile.bind(this);
this.clearSelection = this.clearSelection.bind(this);
this.cancelPicking = this.cancelPicking.bind(this);
} // eslint-disable-next-line class-methods-use-this
providerFileToId(file) {
return generateFileID({
data: file,
name: file.name || file.id,
type: file.mimetype
});
}

@@ -78,7 +144,8 @@

}, 'error', 5000);
}
} // todo document what is a "tagFile" or get rid of this concept
addFile(file) {
getTagFile(file) {
const tagFile = {
id: this.providerFileToId(file),
id: file.id,
source: this.plugin.id,

@@ -100,3 +167,4 @@ data: file,

providerOptions: this.provider.opts,
providerName: this.provider.name
providerName: this.provider.name,
provider: this.provider.provider
}

@@ -115,16 +183,11 @@ };

this.plugin.uppy.log('Adding remote file');
return tagFile;
}
try {
this.plugin.uppy.addFile(tagFile);
return true;
} catch (err) {
if (!err.isRestriction) {
this.plugin.uppy.log(err);
}
return false;
}
setLoading(loading) {
this.plugin.setPluginState({
loading
});
}
}
{
"name": "@uppy/provider-views",
"description": "View library for Uppy remote provider plugins.",
"version": "3.1.0",
"version": "3.2.0",
"license": "MIT",

@@ -23,9 +23,10 @@ "main": "lib/index.js",

"dependencies": {
"@uppy/utils": "^5.1.3",
"@uppy/utils": "^5.2.0",
"classnames": "^2.2.6",
"nanoid": "^4.0.0",
"preact": "^10.5.13"
},
"peerDependencies": {
"@uppy/core": "^3.1.0"
"@uppy/core": "^3.1.2"
}
}
import { h } from 'preact'
import classNames from 'classnames'
import remoteFileObjToLocal from '@uppy/utils/lib/remoteFileObjToLocal'
import Filter from './Filter.jsx'
import SearchFilterInput from './SearchFilterInput.jsx'
import FooterActions from './FooterActions.jsx'

@@ -29,5 +27,10 @@ import Item from './Item/index.jsx'

validateRestrictions,
showFilter,
filterQuery,
filterInput,
isLoading,
showSearchFilter,
search,
searchTerm,
clearSearch,
searchOnInput,
searchInputLabel,
clearSearchLabel,
getNextFolder,

@@ -37,2 +40,3 @@ cancel,

columns,
noResultsLabel,
} = props

@@ -49,26 +53,42 @@

>
<div className="uppy-ProviderBrowser-header">
<div
className={classNames(
'uppy-ProviderBrowser-headerBar',
!showBreadcrumbs && 'uppy-ProviderBrowser-headerBar--simple',
)}
>
{headerComponent}
{headerComponent && (
<div className="uppy-ProviderBrowser-header">
<div
className={classNames(
'uppy-ProviderBrowser-headerBar',
!showBreadcrumbs && 'uppy-ProviderBrowser-headerBar--simple',
)}
>
{headerComponent}
</div>
</div>
</div>
)}
{showFilter && (
<Filter
i18n={i18n}
filterQuery={filterQuery}
filterInput={filterInput}
/>
{showSearchFilter && (
<div class="uppy-ProviderBrowser-searchFilter">
<SearchFilterInput
search={search}
searchTerm={searchTerm}
clearSearch={clearSearch}
inputLabel={searchInputLabel}
clearSearchLabel={clearSearchLabel}
inputClassName="uppy-ProviderBrowser-searchFilterInput"
searchOnInput={searchOnInput}
/>
</div>
)}
{(() => {
if (isLoading) {
return (
<div className="uppy-Provider-loading">
<span>{i18n('loading')}</span>
</div>
)
}
if (!folders.length && !files.length) {
return (
<div className="uppy-Provider-empty">
{i18n('noFilesFound')}
{noResultsLabel}
</div>

@@ -75,0 +95,0 @@ )

import { h } from 'preact'
import classNames from 'classnames'

@@ -18,2 +19,9 @@ function GridListItem (props) {

const checkBoxClassName = classNames(
'uppy-u-reset',
'uppy-ProviderBrowserItem-checkbox',
'uppy-ProviderBrowserItem-checkbox--grid',
{ 'uppy-ProviderBrowserItem-checkbox--is-checked': isChecked },
)
return (

@@ -26,5 +34,3 @@ <li

type="checkbox"
className={`uppy-u-reset uppy-ProviderBrowserItem-checkbox ${
isChecked ? 'uppy-ProviderBrowserItem-checkbox--is-checked' : ''
} uppy-ProviderBrowserItem-checkbox--grid`}
className={checkBoxClassName}
onChange={toggleCheckbox}

@@ -43,9 +49,5 @@ onKeyDown={recordShiftKeyPress}

>
<span className="uppy-ProviderBrowserItem-inner-relative">
{itemIconEl}
{showTitles && title}
{children}
</span>
{itemIconEl}
{showTitles && title}
{children}
</label>

@@ -52,0 +54,0 @@ </li>

@@ -45,2 +45,3 @@ import { h } from 'preact'

className="uppy-ProviderBrowserItem-author"
tabIndex="-1"
>

@@ -47,0 +48,0 @@ {author.name}

import { h } from 'preact'
import { getSafeFileId } from '@uppy/utils/lib/generateFileID'
import AuthView from './AuthView.jsx'

@@ -56,2 +58,3 @@ import Header from './Header.jsx'

this.filterQuery = this.filterQuery.bind(this)
this.clearFilter = this.clearFilter.bind(this)
this.getFolder = this.getFolder.bind(this)

@@ -62,3 +65,2 @@ this.getNextFolder = this.getNextFolder.bind(this)

this.handleScroll = this.handleScroll.bind(this)
this.listAllFiles = this.listAllFiles.bind(this)
this.donePicking = this.donePicking.bind(this)

@@ -105,25 +107,27 @@

*/
getFolder (id, name) {
return this.sharedHandler.loaderWrapper(
this.provider.list(id),
(res) => {
const folders = []
const files = []
let updatedDirectories
async getFolder (id, name) {
this.setLoading(true)
try {
const res = await this.provider.list(id)
const folders = []
const files = []
let updatedDirectories
const state = this.plugin.getPluginState()
const index = state.directories.findIndex((dir) => id === dir.id)
const state = this.plugin.getPluginState()
const index = state.directories.findIndex((dir) => id === dir.id)
if (index !== -1) {
updatedDirectories = state.directories.slice(0, index + 1)
} else {
updatedDirectories = state.directories.concat([{ id, title: name }])
}
if (index !== -1) {
updatedDirectories = state.directories.slice(0, index + 1)
} else {
updatedDirectories = state.directories.concat([{ id, title: name }])
}
this.username = res.username || this.username
this.#updateFilesAndFolders(res, files, folders)
this.plugin.setPluginState({ directories: updatedDirectories, filterInput: '' })
},
this.handleError,
)
this.username = res.username || this.username
this.#updateFilesAndFolders(res, files, folders)
this.plugin.setPluginState({ directories: updatedDirectories, filterInput: '' })
} catch (err) {
this.handleError(err)
} finally {
this.setLoading(false)
}
}

@@ -168,73 +172,8 @@

filterQuery (e) {
const state = this.plugin.getPluginState()
this.plugin.setPluginState({ ...state, filterInput: e ? e.target.value : '' })
filterQuery (input) {
this.plugin.setPluginState({ filterInput: input })
}
/**
* Adds all files found inside of specified folder.
*
* Uses separated state while folder contents are being fetched and
* mantains list of selected folders, which are separated from files.
*/
addFolder (folder) {
const folderId = this.providerFileToId(folder)
const folders = { ...this.plugin.getPluginState().selectedFolders }
if (folderId in folders && folders[folderId].loading) {
return
}
folders[folderId] = { loading: true, files: [] }
this.plugin.setPluginState({ selectedFolders: { ...folders } })
// eslint-disable-next-line consistent-return
return this.listAllFiles(folder.requestPath).then((files) => {
let count = 0
// If the same folder is added again, we don't want to send
// X amount of duplicate file notifications, we want to say
// the folder was already added. This checks if all files are duplicate,
// if that's the case, we don't add the files.
files.forEach(file => {
const id = this.providerFileToId(file)
if (!this.plugin.uppy.checkIfFileAlreadyExists(id)) {
count++
}
})
if (count > 0) {
files.forEach((file) => this.addFile(file))
}
const ids = files.map(this.providerFileToId)
folders[folderId] = {
loading: false,
files: ids,
}
this.plugin.setPluginState({ selectedFolders: folders, filterInput: '' })
let message
if (count === 0) {
message = this.plugin.uppy.i18n('folderAlreadyAdded', {
folder: folder.name,
})
} else if (files.length) {
message = this.plugin.uppy.i18n('folderAdded', {
smart_count: count, folder: folder.name,
})
} else {
message = this.plugin.uppy.i18n('emptyFolderAdded')
}
this.plugin.uppy.info(message)
}).catch((e) => {
const selectedFolders = { ...this.plugin.getPluginState().selectedFolders }
delete selectedFolders[folderId]
this.plugin.setPluginState({ selectedFolders })
this.handleError(e)
})
clearFilter () {
this.plugin.setPluginState({ filterInput: '' })
}

@@ -303,31 +242,87 @@

async listAllFiles (path, files = null) {
files = files || [] // eslint-disable-line no-param-reassign
const res = await this.provider.list(path)
res.items.forEach((item) => {
if (!item.isFolder) {
files.push(item)
} else {
this.addFolder(item)
async* recursivelyListAllFiles (path) {
let curPath = path
// need to repeat the list call until there are no more pages
while (curPath) {
const res = await this.provider.list(curPath)
for (const item of res.items) {
if (item.isFolder) {
// recursively call self for folder
yield* this.recursivelyListAllFiles(item.requestPath)
} else {
yield item
}
}
})
const moreFiles = res.nextPagePath
if (moreFiles) {
return this.listAllFiles(moreFiles, files)
curPath = res.nextPagePath
}
return files
}
donePicking () {
const { currentSelection } = this.plugin.getPluginState()
const promises = currentSelection.map((file) => {
if (file.isFolder) {
return this.addFolder(file)
async donePicking () {
this.setLoading(true)
try {
const { currentSelection } = this.plugin.getPluginState()
const messages = []
const newFiles = []
// eslint-disable-next-line no-unreachable-loop
for (const file of currentSelection) {
if (file.isFolder) {
const { requestPath, name } = file
let isEmpty = true
let numNewFiles = 0
for await (const fileInFolder of this.recursivelyListAllFiles(requestPath)) {
const tagFile = this.getTagFile(fileInFolder)
const id = getSafeFileId(tagFile)
// If the same folder is added again, we don't want to send
// X amount of duplicate file notifications, we want to say
// the folder was already added. This checks if all files are duplicate,
// if that's the case, we don't add the files.
if (!this.plugin.uppy.checkIfFileAlreadyExists(id)) {
newFiles.push(fileInFolder)
numNewFiles++
}
isEmpty = false
}
let message
if (isEmpty) {
message = this.plugin.uppy.i18n('emptyFolderAdded')
} else if (numNewFiles === 0) {
message = this.plugin.uppy.i18n('folderAlreadyAdded', {
folder: name,
})
} else {
message = this.plugin.uppy.i18n('folderAdded', {
smart_count: numNewFiles, folder: name,
})
}
messages.push(message)
} else {
newFiles.push(file)
}
}
return this.addFile(file)
})
this.sharedHandler.loaderWrapper(Promise.all(promises), () => {
// Note: this.plugin.uppy.addFiles must be only run once we are done fetching all files,
// because it will cause the loading screen to disappear,
// and that will allow the user to start the upload, so we need to make sure we have
// finished all async operations before we add any file
// see https://github.com/transloadit/uppy/pull/4384
this.plugin.uppy.log('Adding remote provider files')
this.plugin.uppy.addFiles(newFiles.map((file) => this.getTagFile(file)))
this.plugin.setPluginState({ filterInput: '' })
messages.forEach(message => this.plugin.uppy.info(message))
this.clearSelection()
}, () => {})
} catch (err) {
this.handleError(err)
} finally {
this.setLoading(false)
}
}

@@ -337,2 +332,3 @@

const { authenticated, didFirstRender } = this.plugin.getPluginState()
const { i18n } = this.plugin.uppy

@@ -345,3 +341,3 @@ if (!didFirstRender) {

const { files, folders, filterInput, loading, currentSelection } = this.plugin.getPluginState()
const { isChecked, toggleCheckbox, recordShiftKeyPress, filterItems } = this.sharedHandler
const { isChecked, toggleCheckbox, recordShiftKeyPress, filterItems } = this
const hasInput = filterInput !== ''

@@ -356,3 +352,3 @@ const headerProps = {

username: this.username,
i18n: this.plugin.uppy.i18n,
i18n,
}

@@ -370,7 +366,15 @@

getFolder: this.getFolder,
filterItems: this.sharedHandler.filterItems,
filterQuery: this.filterQuery,
// For SearchFilterInput component
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,
listAllFiles: this.listAllFiles,
done: this.donePicking,

@@ -382,3 +386,2 @@ cancel: this.cancelPicking,

showTitles: targetViewOptions.showTitles,
showFilter: targetViewOptions.showFilter,
showBreadcrumbs: targetViewOptions.showBreadcrumbs,

@@ -385,0 +388,0 @@ pluginIcon: this.plugin.icon,

import { h } from 'preact'
import SearchInput from './InputView.jsx'
import SearchFilterInput from '../SearchFilterInput.jsx'
import Browser from '../Browser.jsx'
import LoaderView from '../Loader.jsx'
import Header from './Header.jsx'
import CloseWrapper from '../CloseWrapper.js'

@@ -13,3 +11,4 @@ import View from '../View.js'

/**
* Class to easily generate generic views for Provider plugins
* SearchProviderView, used for Unsplash and future image search providers.
* Extends generic View, shared with regular providers like Google Drive and Instagram.
*/

@@ -39,4 +38,4 @@ export default class SearchProviderView extends View {

this.search = this.search.bind(this)
this.triggerSearchInput = this.triggerSearchInput.bind(this)
this.addFile = this.addFile.bind(this)
this.clearSearch = this.clearSearch.bind(this)
this.resetPluginState = this.resetPluginState.bind(this)
this.handleScroll = this.handleScroll.bind(this)

@@ -48,4 +47,3 @@ this.donePicking = this.donePicking.bind(this)

// Set default state for the plugin
this.plugin.setPluginState({
this.defaultState = {
isInputMode: true,

@@ -58,3 +56,6 @@ files: [],

searchTerm: null,
})
}
// Set default state for the plugin
this.plugin.setPluginState(this.defaultState)
}

@@ -67,9 +68,4 @@

clearSelection () {
this.plugin.setPluginState({
currentSelection: [],
isInputMode: true,
files: [],
searchTerm: null,
})
resetPluginState () {
this.plugin.setPluginState(this.defaultState)
}

@@ -81,2 +77,3 @@

this.plugin.setPluginState({
currentSelection: [],
isInputMode: false,

@@ -88,20 +85,26 @@ files,

search (query) {
async search (query) {
const { searchTerm } = this.plugin.getPluginState()
if (query && query === searchTerm) {
// no need to search again as this is the same as the previous search
return undefined
return
}
return this.sharedHandler.loaderWrapper(
this.provider.search(query),
(res) => {
this.#updateFilesAndInputMode(res, [])
},
this.handleError,
)
this.setLoading(true)
try {
const res = await this.provider.search(query)
this.#updateFilesAndInputMode(res, [])
} catch (err) {
this.handleError(err)
} finally {
this.setLoading(false)
}
}
triggerSearchInput () {
this.plugin.setPluginState({ isInputMode: true })
clearSearch () {
this.plugin.setPluginState({
currentSelection: [],
files: [],
searchTerm: null,
})
}

@@ -130,7 +133,5 @@

const { currentSelection } = this.plugin.getPluginState()
const promises = currentSelection.map((file) => this.addFile(file))
this.sharedHandler.loaderWrapper(Promise.all(promises), () => {
this.clearSelection()
}, () => {})
this.plugin.uppy.log('Adding remote search provider files')
this.plugin.uppy.addFiles(currentSelection.map((file) => this.getTagFile(file)))
this.resetPluginState()
}

@@ -140,2 +141,3 @@

const { didFirstRender, isInputMode, searchTerm } = this.plugin.getPluginState()
const { i18n } = this.plugin.uppy

@@ -148,3 +150,3 @@ if (!didFirstRender) {

const { files, folders, filterInput, loading, currentSelection } = this.plugin.getPluginState()
const { isChecked, toggleCheckbox, filterItems } = this.sharedHandler
const { isChecked, toggleCheckbox, filterItems } = this
const hasInput = filterInput !== ''

@@ -161,7 +163,13 @@

cancel: this.cancelPicking,
headerComponent: Header({
search: this.search,
i18n: this.plugin.uppy.i18n,
searchTerm,
}),
// For SearchFilterInput component
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,

@@ -171,5 +179,6 @@ viewType: targetViewOptions.viewType,

showFilter: targetViewOptions.showFilter,
isLoading: loading,
showBreadcrumbs: targetViewOptions.showBreadcrumbs,
pluginIcon: this.plugin.icon,
i18n: this.plugin.uppy.i18n,
i18n,
uppyFiles: this.plugin.uppy.getFiles(),

@@ -179,17 +188,16 @@ validateRestrictions: (...args) => this.plugin.uppy.validateRestrictions(...args),

if (loading) {
return (
<CloseWrapper onUnmount={this.clearSelection}>
<LoaderView i18n={this.plugin.uppy.i18n} />
</CloseWrapper>
)
}
if (isInputMode) {
return (
<CloseWrapper onUnmount={this.clearSelection}>
<SearchInput
search={this.search}
i18n={this.plugin.uppy.i18n}
/>
<CloseWrapper onUnmount={this.resetPluginState}>
<div className="uppy-SearchProvider">
<SearchFilterInput
search={this.search}
clearSelection={this.clearSelection}
inputLabel={i18n('enterTextToSearch')}
buttonLabel={i18n('searchImages')}
inputClassName="uppy-c-textInput uppy-SearchProvider-input"
buttonCSSClassName="uppy-SearchProvider-searchButton"
showButton
/>
</div>
</CloseWrapper>

@@ -200,3 +208,3 @@ )

return (
<CloseWrapper onUnmount={this.clearSelection}>
<CloseWrapper onUnmount={this.resetPluginState}>
{/* eslint-disable-next-line react/jsx-props-no-spreading */}

@@ -203,0 +211,0 @@ <Browser {...browserProps} />

import getFileType from '@uppy/utils/lib/getFileType'
import isPreviewSupported from '@uppy/utils/lib/isPreviewSupported'
import generateFileID from '@uppy/utils/lib/generateFileID'
import remoteFileObjToLocal from '@uppy/utils/lib/remoteFileObjToLocal'
// TODO: now that we have a shared `View` class,
// `SharedHandler` could be cleaned up and moved into here
import SharedHandler from './SharedHandler.js'
export default class View {

@@ -13,3 +9,2 @@ constructor (plugin, opts) {

this.provider = opts.provider
this.sharedHandler = new SharedHandler(plugin)

@@ -20,3 +15,2 @@ this.isHandlingScroll = false

this.handleError = this.handleError.bind(this)
this.addFile = this.addFile.bind(this)
this.clearSelection = this.clearSelection.bind(this)

@@ -26,11 +20,2 @@ this.cancelPicking = this.cancelPicking.bind(this)

// eslint-disable-next-line class-methods-use-this
providerFileToId (file) {
return generateFileID({
data: file,
name: file.name || file.id,
type: file.mimetype,
})
}
preFirstRender () {

@@ -76,5 +61,6 @@ this.plugin.setPluginState({ didFirstRender: true })

addFile (file) {
// todo document what is a "tagFile" or get rid of this concept
getTagFile (file) {
const tagFile = {
id: this.providerFileToId(file),
id: file.id,
source: this.plugin.id,

@@ -97,2 +83,3 @@ data: file,

providerName: this.provider.name,
provider: this.provider.provider,
},

@@ -113,14 +100,84 @@ }

this.plugin.uppy.log('Adding remote file')
return tagFile
}
try {
this.plugin.uppy.addFile(tagFile)
return true
} catch (err) {
if (!err.isRestriction) {
this.plugin.uppy.log(err)
filterItems = (items) => {
const state = this.plugin.getPluginState()
if (!state.filterInput || state.filterInput === '') {
return items
}
return items.filter((folder) => {
return folder.name.toLowerCase().indexOf(state.filterInput.toLowerCase()) !== -1
})
}
recordShiftKeyPress = (e) => {
this.isShiftKeyPressed = e.shiftKey
}
/**
* Toggles file/folder checkbox to on/off state while updating files list.
*
* Note that some extra complexity comes from supporting shift+click to
* toggle multiple checkboxes at once, which is done by getting all files
* in between last checked file and current one.
*/
toggleCheckbox = (e, file) => {
e.stopPropagation()
e.preventDefault()
e.currentTarget.focus()
const { folders, files } = this.plugin.getPluginState()
const items = this.filterItems(folders.concat(files))
// Shift-clicking selects a single consecutive list of items
// starting at the previous click and deselects everything else.
if (this.lastCheckbox && this.isShiftKeyPressed) {
const prevIndex = items.indexOf(this.lastCheckbox)
const currentIndex = items.indexOf(file)
const currentSelection = (prevIndex < currentIndex)
? items.slice(prevIndex, currentIndex + 1)
: items.slice(currentIndex, prevIndex + 1)
const reducedCurrentSelection = []
// Check restrictions on each file in currentSelection,
// reduce it to only contain files that pass restrictions
for (const item of currentSelection) {
const { uppy } = this.plugin
const restrictionError = uppy.validateRestrictions(
remoteFileObjToLocal(item),
[...uppy.getFiles(), ...reducedCurrentSelection],
)
if (!restrictionError) {
reducedCurrentSelection.push(item)
} else {
uppy.info({ message: restrictionError.message }, 'error', uppy.opts.infoTimeout)
}
}
return false
this.plugin.setPluginState({ currentSelection: reducedCurrentSelection })
return
}
this.lastCheckbox = file
const { currentSelection } = this.plugin.getPluginState()
if (this.isChecked(file)) {
this.plugin.setPluginState({
currentSelection: currentSelection.filter((item) => item.id !== file.id),
})
} else {
this.plugin.setPluginState({
currentSelection: currentSelection.concat([file]),
})
}
}
isChecked = (file) => {
const { currentSelection } = this.plugin.getPluginState()
// comparing id instead of the file object, because the reference to the object
// changes when we switch folders, and the file list is updated
return currentSelection.some((item) => item.id === file.id)
}
setLoading (loading) {
this.plugin.setPluginState({ loading })
}
}

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc