@uppy/provider-views
Version:
View library for Uppy remote provider plugins.
410 lines (402 loc) • 14.4 kB
JavaScript
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 { h } from 'preact';
import classNames from 'classnames';
import remoteFileObjToLocal from '@uppy/utils/lib/remoteFileObjToLocal';
import AuthView from "./AuthView.js";
import Header from "./Header.js";
import Browser from "../Browser.js";
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore We don't want TS to generate types for the package.json
const packageJson = {
"version": "4.4.2"
};
import PartialTreeUtils from '../utils/PartialTreeUtils/index.js';
import shouldHandleScroll from '../utils/shouldHandleScroll.js';
import handleError from '../utils/handleError.js';
import getClickedRange from '../utils/getClickedRange.js';
import SearchInput from "../SearchInput.js";
import FooterActions from "../FooterActions.js";
import addFiles from '../utils/addFiles.js';
import getCheckedFilesWithPaths from '../utils/PartialTreeUtils/getCheckedFilesWithPaths.js';
import getBreadcrumbs from '../utils/PartialTreeUtils/getBreadcrumbs.js';
export function defaultPickerIcon() {
return h("svg", {
"aria-hidden": "true",
focusable: "false",
width: "30",
height: "30",
viewBox: "0 0 30 30"
}, h("path", {
d: "M15 30c8.284 0 15-6.716 15-15 0-8.284-6.716-15-15-15C6.716 0 0 6.716 0 15c0 8.284 6.716 15 15 15zm4.258-12.676v6.846h-8.426v-6.846H5.204l9.82-12.364 9.82 12.364H19.26z"
}));
}
const getDefaultState = rootFolderId => ({
authenticated: undefined,
// we don't know yet
partialTree: [{
type: 'root',
id: rootFolderId,
cached: false,
nextPagePath: null
}],
currentFolderId: rootFolderId,
searchString: '',
didFirstRender: false,
username: null,
loading: false
});
var _abortController = /*#__PURE__*/_classPrivateFieldLooseKey("abortController");
var _withAbort = /*#__PURE__*/_classPrivateFieldLooseKey("withAbort");
/**
* Class to easily generate generic views for Provider plugins
*/
export default class ProviderView {
constructor(plugin, opts) {
Object.defineProperty(this, _withAbort, {
value: _withAbort2
});
this.isHandlingScroll = false;
this.lastCheckbox = null;
Object.defineProperty(this, _abortController, {
writable: true,
value: void 0
});
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 => {
var _item$data$name;
return ((_item$data$name = item.data.name) != null ? _item$data$name : this.plugin.uppy.i18n('unnamed')).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.cancelSelection = this.cancelSelection.bind(this);
this.toggleCheckbox = this.toggleCheckbox.bind(this);
// Set default state for the plugin
this.resetPluginState();
// todo
// @ts-expect-error this should be typed in @uppy/dashboard.
this.plugin.uppy.on('dashboard:close-panel', this.resetPluginState);
this.plugin.uppy.registerRequestClient(this.provider.provider, this.provider);
}
resetPluginState() {
this.plugin.setPluginState(getDefaultState(this.plugin.rootFolderId));
}
// eslint-disable-next-line class-methods-use-this
tearDown() {
// Nothing.
}
setLoading(loading) {
this.plugin.setPluginState({
loading
});
}
cancelSelection() {
const {
partialTree
} = this.plugin.getPluginState();
const newPartialTree = partialTree.map(item => item.type === 'root' ? item : {
...item,
status: 'unchecked'
});
this.plugin.setPluginState({
partialTree: newPartialTree
});
}
async openFolder(folderId) {
this.lastCheckbox = null;
// Returning cached folder
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
});
// It's important to set the username during one of our first fetches
this.plugin.setPluginState({
username
});
currentPagePath = nextPagePath;
currentItems = currentItems.concat(items);
this.setLoading(this.plugin.uppy.i18n('loadedXFiles', {
numFiles: currentItems.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(handleError(this.plugin.uppy));
this.setLoading(false);
}
/**
* Removes session token on client side.
*/
async logout() {
await _classPrivateFieldLooseBase(this, _withAbort)[_withAbort](async signal => {
const res = await this.provider.logout({
signal
});
// res.ok is from the JSON body, not to be confused with Response.ok
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
});
}
}).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) {
const {
partialTree,
currentFolderId
} = this.plugin.getPluginState();
const currentFolder = partialTree.find(i => i.id === currentFolderId);
if (shouldHandleScroll(event) && !this.isHandlingScroll && currentFolder.nextPagePath) {
this.isHandlingScroll = true;
await _classPrivateFieldLooseBase(this, _withAbort)[_withAbort](async signal => {
const {
nextPagePath,
items
} = await this.provider.list(currentFolder.nextPagePath, {
signal
});
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);
await _classPrivateFieldLooseBase(this, _withAbort)[_withAbort](async signal => {
// 1. Enrich our partialTree by fetching all 'checked' but not-yet-fetched folders
const enrichedTree = await PartialTreeUtils.afterFill(partialTree, path => this.provider.list(path, {
signal
}), this.validateSingleFile, n => {
this.setLoading(this.plugin.uppy.i18n('addedNumFiles', {
numFiles: n
}));
});
// 2. Now that we know how many files there are - recheck aggregateRestrictions!
const aggregateRestrictionError = this.validateAggregateRestrictions(enrichedTree);
if (aggregateRestrictionError) {
this.plugin.setPluginState({
partialTree: enrichedTree
});
return;
}
// 3. Add files
const companionFiles = getCheckedFilesWithPaths(enrichedTree);
addFiles(companionFiles, this.plugin, this.provider);
// 4. Reset state
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) {
if (viewOptions === void 0) {
viewOptions = {};
}
const {
didFirstRender
} = this.plugin.getPluginState();
const {
i18n
} = this.plugin.uppy;
if (!didFirstRender) {
this.plugin.setPluginState({
didFirstRender: true
});
this.provider.fetchPreAuthToken();
this.openFolder(this.plugin.rootFolderId);
}
const opts = {
...this.opts,
...viewOptions
};
const {
authenticated,
loading
} = this.plugin.getPluginState();
const pluginIcon = this.plugin.icon || defaultPickerIcon;
if (authenticated === false) {
return h(AuthView, {
pluginName: this.plugin.title,
pluginIcon: pluginIcon,
handleAuth: this.handleAuth,
i18n: this.plugin.uppy.i18n,
renderForm: opts.renderAuthForm,
loading: loading
});
}
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,
utmSource: "Companion"
}), h(FooterActions, {
partialTree: partialTree,
donePicking: this.donePicking,
cancelSelection: this.cancelSelection,
i18n: i18n,
validateAggregateRestrictions: this.validateAggregateRestrictions
}));
}
}
async function _withAbort2(op) {
var _classPrivateFieldLoo;
// prevent multiple requests in parallel from causing race conditions
(_classPrivateFieldLoo = _classPrivateFieldLooseBase(this, _abortController)[_abortController]) == null || _classPrivateFieldLoo.abort();
const abortController = new AbortController();
_classPrivateFieldLooseBase(this, _abortController)[_abortController] = abortController;
const cancelRequest = () => {
abortController.abort();
};
try {
// @ts-expect-error this should be typed in @uppy/dashboard.
// Even then I don't think we can make this work without adding dashboard
// as a dependency to provider-views.
this.plugin.uppy.on('dashboard:close-panel', cancelRequest);
this.plugin.uppy.on('cancel-all', cancelRequest);
await op(abortController.signal);
} finally {
// @ts-expect-error this should be typed in @uppy/dashboard.
// Even then I don't think we can make this work without adding dashboard
// as a dependency to provider-views.
this.plugin.uppy.off('dashboard:close-panel', cancelRequest);
this.plugin.uppy.off('cancel-all', cancelRequest);
_classPrivateFieldLooseBase(this, _abortController)[_abortController] = undefined;
}
}
ProviderView.VERSION = packageJson.version;