UNPKG

@uppy/provider-views

Version:

View library for Uppy remote provider plugins.

279 lines (278 loc) 14.1 kB
import { jsx as _jsx, jsxs as _jsxs } from "preact/jsx-runtime"; import { remoteFileObjToLocal } from '@uppy/utils'; import classNames from 'classnames'; import packageJson from '../../package.json' with { type: 'json' }; import Browser from '../Browser.js'; 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 getNumberOfSelectedFiles from '../utils/PartialTreeUtils/getNumberOfSelectedFiles.js'; import PartialTreeUtils from '../utils/PartialTreeUtils/index.js'; import shouldHandleScroll from '../utils/shouldHandleScroll.js'; import AuthView from './AuthView.js'; import Header from './Header.js'; export function defaultPickerIcon() { return (_jsx("svg", { "aria-hidden": "true", focusable: "false", width: "30", height: "30", viewBox: "0 0 30 30", children: _jsx("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, }); /** * Class to easily generate generic views for Provider plugins */ export default class ProviderView { static VERSION = packageJson.version; plugin; provider; opts; isHandlingScroll = false; lastCheckbox = null; constructor(plugin, opts) { 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)); } 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 }); } #abortController; async #withAbort(op) { // prevent multiple requests in parallel from causing race conditions this.#abortController?.abort(); const abortController = new AbortController(); this.#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); this.#abortController = undefined; } } 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 this.#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 this.#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 this.#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 this.#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; } } validateSingleFile = (file) => { const companionFile = remoteFileObjToLocal(file); const result = this.plugin.uppy.validateSingleFile(companionFile); return result; }; async donePicking() { const { partialTree } = this.plugin.getPluginState(); this.setLoading(true); await this.#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; } 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 ?? this.plugin.uppy.i18n('unnamed')) .toLowerCase() .indexOf(searchString.toLowerCase()) !== -1); return filtered; }; getBreadcrumbs = () => { const { partialTree, currentFolderId } = this.plugin.getPluginState(); return getBreadcrumbs(partialTree, currentFolderId); }; getSelectedAmount = () => { const { partialTree } = this.plugin.getPluginState(); return getNumberOfSelectedFiles(partialTree); }; 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); }; render(state, 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 (_jsx(AuthView, { pluginName: this.plugin.title, pluginIcon: pluginIcon, handleAuth: this.handleAuth, i18n: this.plugin.uppy.i18n, renderForm: opts.renderAuthForm, loading: loading })); } const { partialTree, username, searchString } = this.plugin.getPluginState(); const breadcrumbs = this.getBreadcrumbs(); return (_jsxs("div", { className: classNames('uppy-ProviderBrowser', `uppy-ProviderBrowser-viewType--${opts.viewType}`), children: [_jsx(Header, { showBreadcrumbs: opts.showBreadcrumbs, openFolder: this.openFolder, breadcrumbs: breadcrumbs, pluginIcon: pluginIcon, title: this.plugin.title, logout: this.logout, username: username, i18n: i18n }), opts.showFilter && (_jsx(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" })), _jsx(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" }), _jsx(FooterActions, { partialTree: partialTree, donePicking: this.donePicking, cancelSelection: this.cancelSelection, i18n: i18n, validateAggregateRestrictions: this.validateAggregateRestrictions })] })); } }