From 4dc28609d728e2cfeb8b54685e5f5c2b26db6539 Mon Sep 17 00:00:00 2001 From: Evgenia Karunus Date: Fri, 21 Jun 2024 04:43:59 +0400 Subject: [PATCH] Provider views rewrite (.files, .folders => .partialTree) (#5050) * ProviderView.tsx - fix onedrive breadcrumbs * providers - correct ch-unch-indeterminate states * providers - made .breadcrumbs derived from .partialTree * everywhere - { files, folders, isChecked } => .partialTree GoogleDrive - travelling down into folders works - checking a file works - breadcrumbs DONT work * GoogleDrive - made breadcrumbs work * .getFolder() - remove the `name` argument * - refactors "/" * Instagram - made files get fetched onScroll * clearSelection() - recover the functionality * GoogleDrive - recover custom `.toggleCheckbox()` functionality * providers - recover `.isDisabled` functionality * - made Unsplash use .partialTree * Facebook - change `.files, .folders` => `.partialTree` * everywhere - we don't need to ! `partialTreeFile.data` anymore * - implement folder caching * - enable shift-clicking * everywhere - get rid of unnecessary `.getNextFolder()` * everywhere - fixing types * - rename `requestPath` to `folderId` * all providers - get rid of `.onFirstRender()` * provider views - get rid of `.onFirstRender()` * - make the root folder cacheable too * TEMP - setup for working with FOLDERS + LAZY_LOADING * - get rid of `.#listFilesAndFolders` * - make `this.nextPagePath` per-folder * everywhere - more refined types * types - reintroduce `StatusInPartialTree` * - made Unsplash work with the new structure * - preemptive cleaning of `.absDirPath` and `.relDirPath` * - give `.nextPagePath` a rigorous type * - make `.nextPagePath` & `.cached` a composite key * - remove unnecessary indirection level * css - factor out `.statusClassName` * everywhere - refactor `.validateRestrictions()` * nOfSelectedFiles - make "Selected (n)" as smart as possible * - prevent shift-clicking from highlighting file names * `.validateRestrictions()` - make it accept a `CompanionFile` instead of `PartialTree`'s file * `.getFolder()` - simplify code * everywhere - account for `restrictions` in `.partialTree` * `PartialTreeUtils.ts` - factor out `getPartialTreeAfterTogglingCheckboxes()` * `PartialTreeUtils.ts` - factor out `clickOnFolder()` * `PartialTreeUtils.ts` - factor out `getPartialTreeAfterScroll()` * `PartialTreeUtils.ts` - rename methods * `.donePicking()` - implement using recursion * `.donePicking()` - integrate with `` * `donePicking()` - show notifications after addition * `#list()` - get rid of unnecessary indirection * ProviderView.tsx - add `signal` everywhere, reduce try/catch indents everywhere * `handleError()` - make error handling uniform * `state.isSearchVisible` - remove, it's just not used anywhere * state - reuse default state * state - reset state on close panel (like we discussed in the uppy call) * methods - remove unnecessary indirection in state setting * `` - remove CloseWrappers, this is unnecessary indirection now too * `this.requestClientId` - remove, again - this was unnecessary indirection * `getTagFile()` - factor out into a separate file * `recordShiftKeyPress()` - fix chaotic shift-clicking in Grid providers, remove endless prop drilling while we're at it * `getNOfSelectedFiles.ts`, `filterItems.ts` - factor out, this removes props drilling * - pass `displayedPartialTree` right away (because Search&NormalProvider have wildly different logics!) * `searchTerm`, `filterInput` - we only need one of these of course! * - fix the issue where `afterToggleCheckbox()` thinks we should always filter by `searchString` * - remove `this.nextPageQuery` Also: fix the issue where upon searching for "ocean" and then "pajama" would just be adding pajama pictures after the ocean ones * - remove unnecessary prop indirection Typescript didn't actually know some of these props aren't used (removed those now)! It only discovers unused props upon normal props passing, like we do now. * - make the form controlled, hugely simplifies everything * `filterItems.ts` - move to , because it's only used there * /utils/PartialTreeUtils.ts - put every util in a separate file * `shouldHandleScroll.ts` - factor out into a util This brings all references to `this.isHandlingScroll` into a single place, and makes `shouldHandleScroll()` a self-contained simple function * this.state - make sure state is reset 1. on cancel 2. on close * `this.xxx` - never leave `this.xxx` variables undefined * `this.username` - should be in `this.state` Also - when there is no username, stop showing the little dot * `SearchProviderPluginState` type - simplify this type, never leave state vars undefined *
- remove completely unnecessary indirection, remove unused props * Facebook.tsx - more sane `viewOptions` code * providers - properly type `opts` * `this.isShiftKeyPressed` - move this variable into * `this.handleError()` - move to /utils * `this.isHandlingScroll` - move to child classes * `this.registerRequestClient()` - move to child classes * `this.lastCheckbox` - move to child classes * `this.setLoading()` - move to child classes * `this.validateRestrictions()` - move to utils * types - fully simplify provider types, remove `View.ts` parent class * index.d.ts - we're not using `OnFirstRenderer` anymore * , - more precise typing for options * package.json - remove nanoid * GoogleDrive - make shift-clicking work * everywhere - fix types across uppy * `afterToggleCheckbox.ts` - less redundant args, pass `ourItem.id` instead of `ourItem` * tests - create `afterToggleCheckbox()` tests * `getClickedRange.ts` - decouple `getClickedRange()` from `afterToggleCheckbox()` * tests - wrote tests for `afterToggleCheckbox.ts` * tests - wrote tests for `afterClickOnFolder.ts` * everywhere - finally rename `getFolder` => `openFolder` * tests - wrote tests for `afterScrollFolder.ts` * getPaths.ts - make `absDirPath`, `relDirPath` work like in docs & add tests for that * injectPaths.ts - improve performance * getTagFile.ts - handle path injection all in one place * getTagFile.ts - refactor Just makes it easier to read the structure of TagFile * fill.ts - `provider.list(currentPath, { signal })` => `apiList` (remove the dependency on provider, just pass a callback) * tests - wrote tests for `fill.ts` * tests - wrote tests for `getNOfSelectedFiles.ts` * everywhere - change `JSON.stringify()` => `clone()` * `PartialTreeUtils.ts` - more consistent function naming + alphabetical order in tests * `donePicking()` - superseded a notification to i18n one * GoogleDrive - make the shared drive checkable * `Item.tsx` - standardize names; remove unnecessary question marks from props * ProviderView.tsx - clicking "Cancel" should make all files "unchecked" * everywhere - move `document.getSelection()?.removeAllRanges()` to to avoid repetition * everywhere - standardize names and types of passed props * - only leave "list of files" to the browser Moves stuff closer to where it's used, prevents props drilling * TEMP - easier pageSize for alex to play with When it's set to 5 pages you have to reduce the browser window to make it scrollable * everywhere - only handle individual-file restrictions * everywhere - add aggregate restrictions on top * SearchProvider, NormalProvider - unite the way we addFiles() Same notifications, same code, same everything * `getTagFile.ts` - pass fewer arguments * `addFiles.ts` - move conversion to tagFiles into `addFiles()` * `uppy.validateRestrictions()` - remove legacy method * `uppy.validateAggregateRestrictions()` - make aggregate restricter report aggregate error * css - make aggregate errors look nice * `PartialTreeUtils/index.test.ts` - accommodate tests to the latest changes * tests - make all uppy tests work * prettiness - run `yarn format` * prettiness - run `yarn lint:fix` * package.json - add `vitest` as a dev dependency * eslint - fixing 1 eslint - fixing 2 eslint - fixing 3 * - add default props as per eslint * - rename to * eslint - fixing 4 (clone.ts) * Uppy.ts - rewrite `partialTree` docs * eslint - fixing 5 * eslint - fixing 6 * `getBreadcrumbs.ts` - factor out * tests - fixing 7 * everywhere - remove `.toReversed()`, because it's not yet supported in all browsers * dev/Dashboard.js - restore to pristine version * prettiness - run `yarn format` * fixing 8 (`yarn run build:ts`) * fixing 9 (run `corepack yarn`) * prettier - undo indentation harm done by prettier * `getBreadcrumbs()` - add tests, and rewrite to avoid using `.toReversed()` Clarification: `.toReversed()` is no supported by all browsers * `` - make it work for eslint * everywhere - remove `eslint-disable react/require-default-props` * , - refactor to avoid prop drilling * - disable checkboxes for GoogleDrive team drives See #5232 * merge (fixing up some lines from the previous merge) * merge (fixing up some lines from the previous merge) * everywhere - remove TEMP development values * `this.validateSingleFile()` - switch to `.restrictionError` * `afterToggleCheckbox.ts` - refactor, add comments * `afterToggleCheckbox.ts` - refactor to use ids instead of whole objects * `afterToggleCheckbox.ts` - try to satisfy prettier * fixing 10 (try to satisfy `npx webpack`) * fixing 11 (try to satisfy `npx webpack`) * Antoine: use Math.min & Math.max in `getClickedRange()` Co-authored-by: Antoine du Hamel * fixing 12 (run `yarn run format`) * `clone.ts` - rename to `shallowClone.ts` * Antoine: in `package.json`, move `devDependencies` up * Antoine: rename `getNOfSelectedFiles()` to `getNumberOfSelectedFiles()` * `getNumberOfSelectedFiles()` - better comments * Antoine: remove `
` tag * Antoine: change `{}` to `Object.create(null)`, write tests * Antoine: `` - return dynamic element * `` - return `buttonCSSClassName` * `GoogleDrive.tsx` - make team drive checkboxes visible * merge (more) * Mifi: update packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx Co-authored-by: Mikael Finstad * merge (more changes) * `Facebook.tsx`, `GooglePhotos.tsx` - render in 'grid' style on per-folder basis * `` - use the `.thumbnail` whenever possible (improves image quality, adds video icons) * `prettier` - ensure `PartialTree` is always strongly indented in tests --------- Co-authored-by: Antoine du Hamel Co-authored-by: Mikael Finstad --- .../@uppy/companion-client/src/Provider.ts | 2 +- packages/@uppy/core/src/Uppy.test.ts | 14 +- packages/@uppy/core/src/Uppy.ts | 123 ++- packages/@uppy/core/src/_common.scss | 6 +- packages/@uppy/facebook/src/Facebook.tsx | 26 +- .../google-drive/src/DriveProviderViews.ts | 20 +- .../@uppy/google-photos/src/GooglePhotos.tsx | 11 +- packages/@uppy/provider-views/package.json | 3 + .../@uppy/provider-views/src/Breadcrumbs.tsx | 52 +- packages/@uppy/provider-views/src/Browser.tsx | 323 ++----- .../@uppy/provider-views/src/CloseWrapper.ts | 13 - .../provider-views/src/FooterActions.tsx | 80 +- .../src/Item/components/GridItem.tsx | 79 +- .../src/Item/components/ListItem.tsx | 132 ++- .../@uppy/provider-views/src/Item/index.tsx | 93 +- .../src/ProviderView/AuthView.tsx | 4 +- .../src/ProviderView/Header.tsx | 42 +- .../src/ProviderView/ProviderView.tsx | 848 ++++++++---------- .../provider-views/src/ProviderView/User.tsx | 10 +- ...{SearchFilterInput.tsx => SearchInput.tsx} | 97 +- .../SearchProviderView/SearchProviderView.tsx | 410 ++++++--- packages/@uppy/provider-views/src/View.ts | 261 ------ packages/@uppy/provider-views/src/style.scss | 23 +- .../uppy-ProviderBrowser-viewType--grid.scss | 2 +- .../uppy-ProviderBrowser-viewType--list.scss | 15 +- .../uppy-ProviderBrowserItem-checkbox.scss | 36 +- .../src/utils/PartialTreeUtils/afterFill.ts | 108 +++ .../utils/PartialTreeUtils/afterOpenFolder.ts | 63 ++ .../PartialTreeUtils/afterScrollFolder.ts | 65 ++ .../PartialTreeUtils/afterToggleCheckbox.ts | 122 +++ .../utils/PartialTreeUtils/getBreadcrumbs.ts | 31 + .../getCheckedFilesWithPaths.ts | 74 ++ .../getNumberOfSelectedFiles.ts | 25 + .../src/utils/PartialTreeUtils/index.test.ts | 612 +++++++++++++ .../src/utils/PartialTreeUtils/index.ts | 11 + .../utils/PartialTreeUtils/shallowClone.ts | 11 + .../provider-views/src/utils/addFiles.ts | 47 + .../src/utils/getClickedRange.ts | 33 + .../provider-views/src/utils/getTagFile.ts | 51 ++ .../provider-views/src/utils/handleError.ts | 29 + .../src/utils/shouldHandleScroll.ts | 8 + .../utils/src/CompanionClientProvider.ts | 12 +- yarn.lock | 3 +- 43 files changed, 2481 insertions(+), 1549 deletions(-) delete mode 100644 packages/@uppy/provider-views/src/CloseWrapper.ts rename packages/@uppy/provider-views/src/{SearchFilterInput.tsx => SearchInput.tsx} (65%) delete mode 100644 packages/@uppy/provider-views/src/View.ts create mode 100644 packages/@uppy/provider-views/src/utils/PartialTreeUtils/afterFill.ts create mode 100644 packages/@uppy/provider-views/src/utils/PartialTreeUtils/afterOpenFolder.ts create mode 100644 packages/@uppy/provider-views/src/utils/PartialTreeUtils/afterScrollFolder.ts create mode 100644 packages/@uppy/provider-views/src/utils/PartialTreeUtils/afterToggleCheckbox.ts create mode 100644 packages/@uppy/provider-views/src/utils/PartialTreeUtils/getBreadcrumbs.ts create mode 100644 packages/@uppy/provider-views/src/utils/PartialTreeUtils/getCheckedFilesWithPaths.ts create mode 100644 packages/@uppy/provider-views/src/utils/PartialTreeUtils/getNumberOfSelectedFiles.ts create mode 100644 packages/@uppy/provider-views/src/utils/PartialTreeUtils/index.test.ts create mode 100644 packages/@uppy/provider-views/src/utils/PartialTreeUtils/index.ts create mode 100644 packages/@uppy/provider-views/src/utils/PartialTreeUtils/shallowClone.ts create mode 100644 packages/@uppy/provider-views/src/utils/addFiles.ts create mode 100644 packages/@uppy/provider-views/src/utils/getClickedRange.ts create mode 100644 packages/@uppy/provider-views/src/utils/getTagFile.ts create mode 100644 packages/@uppy/provider-views/src/utils/handleError.ts create mode 100644 packages/@uppy/provider-views/src/utils/shouldHandleScroll.ts diff --git a/packages/@uppy/companion-client/src/Provider.ts b/packages/@uppy/companion-client/src/Provider.ts index d252c97d12..b8e12dc270 100644 --- a/packages/@uppy/companion-client/src/Provider.ts +++ b/packages/@uppy/companion-client/src/Provider.ts @@ -366,7 +366,7 @@ export default class Provider } list( - directory: string | undefined, + directory: string | null, options: RequestOptions, ): Promise { return this.get(`${this.id}/list/${directory || ''}`, options) diff --git a/packages/@uppy/core/src/Uppy.test.ts b/packages/@uppy/core/src/Uppy.test.ts index b3315fe2c2..25f3736117 100644 --- a/packages/@uppy/core/src/Uppy.test.ts +++ b/packages/@uppy/core/src/Uppy.test.ts @@ -2160,7 +2160,7 @@ describe('src/Core', () => { ) }) - it('should check if a file validateRestrictions', () => { + it('should report error on validateSingleFile', () => { const core = new Core({ restrictions: { minFileSize: 300000, @@ -2185,17 +2185,13 @@ describe('src/Core', () => { size: 270733, } - // @ts-ignore - const validateRestrictions1 = core.validateRestrictions(newFile) - // @ts-ignore - const validateRestrictions2 = core2.validateRestrictions(newFile) + const validateRestrictions1 = core.validateSingleFile(newFile) + const validateRestrictions2 = core2.validateSingleFile(newFile) - expect(validateRestrictions1!.message).toEqual( + expect(validateRestrictions1).toEqual( 'This file is smaller than the allowed size of 293 KB', ) - expect(validateRestrictions2!.message).toEqual( - 'You can only upload: image/png', - ) + expect(validateRestrictions2).toEqual('You can only upload: image/png') }) it('should emit `restriction-failed` event when some rule is violated', () => { diff --git a/packages/@uppy/core/src/Uppy.ts b/packages/@uppy/core/src/Uppy.ts index ae297d6abe..52d05d8c52 100644 --- a/packages/@uppy/core/src/Uppy.ts +++ b/packages/@uppy/core/src/Uppy.ts @@ -58,23 +58,86 @@ export type UnknownPlugin< PluginState extends Record = Record, > = BasePlugin -// `OmitFirstArg` is the type of the returned value of `someArray.slice(1)`. -type OmitFirstArg = T extends [any, ...infer U] ? U : never +/** + * ids are always `string`s, except the root folder's id can be `null` + */ +export type PartialTreeId = string | null + +export type PartialTreeStatusFile = 'checked' | 'unchecked' +export type PartialTreeStatus = PartialTreeStatusFile | 'partial' + +export type PartialTreeFile = { + type: 'file' + id: string + + /** + * There exist two types of restrictions: + * - individual restrictions (`allowedFileTypes`, `minFileSize`, `maxFileSize`), and + * - aggregate restrictions (`maxNumberOfFiles`, `maxTotalFileSize`). + * + * `.restrictionError` reports whether this file passes individual restrictions. + * + */ + restrictionError: string | null + + status: PartialTreeStatusFile + parentId: PartialTreeId + data: CompanionFile +} + +export type PartialTreeFolderNode = { + type: 'folder' + id: string + + /** + * Consider `(.nextPagePath, .cached)` a composite key that can represent 4 states: + * - `{ cached: true, nextPagePath: null }` - we fetched all pages in this folder + * - `{ cached: true, nextPagePath: 'smth' }` - we fetched 1st page, and there are still pages left to fetch in this folder + * - `{ cached: false, nextPagePath: null }` - we didn't fetch the 1st page in this folder + * - `{ cached: false, nextPagePath: 'someString' }` - ❌ CAN'T HAPPEN ❌ + */ + cached: boolean + nextPagePath: PartialTreeId + + status: PartialTreeStatus + parentId: PartialTreeId + data: CompanionFile +} + +export type PartialTreeFolderRoot = { + type: 'root' + id: PartialTreeId + + cached: boolean + nextPagePath: PartialTreeId +} + +export type PartialTreeFolder = PartialTreeFolderNode | PartialTreeFolderRoot + +/** + * PartialTree has the following structure. + * + * FolderRoot + * ┌─────┴─────┐ + * FolderNode File + * ┌─────┴────┐ + * File File + * + * Root folder is called `PartialTreeFolderRoot`, + * all other folders are called `PartialTreeFolderNode`, because they are "internal nodes". + * + * It's possible for `PartialTreeFolderNode` to be a leaf node if it doesn't contain any files. + */ +export type PartialTree = (PartialTreeFile | PartialTreeFolder)[] export type UnknownProviderPluginState = { authenticated: boolean | undefined - breadcrumbs: { - requestPath?: string - name?: string - id?: string - }[] didFirstRender: boolean - currentSelection: CompanionFile[] - filterInput: string + searchString: string loading: boolean | string - folders: CompanionFile[] - files: CompanionFile[] - isSearchVisible: boolean + partialTree: PartialTree + currentFolderId: PartialTreeId + username: string | null } /* * UnknownProviderPlugin can be any Companion plugin (such as Google Drive). @@ -89,8 +152,8 @@ export type UnknownProviderPlugin< M extends Meta, B extends Body, > = UnknownPlugin & { - rootFolderId: string | null title: string + rootFolderId: string | null files: UppyFile[] icon: () => h.JSX.Element provider: CompanionClientProvider @@ -111,16 +174,10 @@ export type UnknownProviderPlugin< * `SearchProvider` does operate on Companion plugins with `uppy.getPlugin()`. */ export type UnknownSearchProviderPluginState = { - isInputMode?: boolean - searchTerm?: string | null + isInputMode: boolean } & Pick< UnknownProviderPluginState, - | 'loading' - | 'files' - | 'folders' - | 'currentSelection' - | 'filterInput' - | 'didFirstRender' + 'loading' | 'searchString' | 'partialTree' | 'currentFolderId' > export type UnknownSearchProviderPlugin< M extends Meta, @@ -296,6 +353,9 @@ export interface UppyEventMap 'upload-start': (files: UppyFile[]) => void } +/** `OmitFirstArg` is the type of the returned value of `someArray.slice(1)`. */ +type OmitFirstArg = T extends [any, ...infer U] ? U : never + const defaultUploadState = { totalProgress: 0, allowNewUpload: true, @@ -780,14 +840,23 @@ export class Uppy> { } } - validateRestrictions( - file: ValidateableFile, - files: ValidateableFile[] = this.getFiles(), - ): RestrictionError | null { + validateSingleFile(file: ValidateableFile): string | null { + try { + this.#restricter.validateSingleFile(file) + } catch (err) { + return err.message + } + return null + } + + validateAggregateRestrictions( + files: ValidateableFile[], + ): string | null { + const existingFiles = this.getFiles() try { - this.#restricter.validate(files, [file]) + this.#restricter.validateAggregateRestrictions(existingFiles, files) } catch (err) { - return err as any + return err.message } return null } diff --git a/packages/@uppy/core/src/_common.scss b/packages/@uppy/core/src/_common.scss index 1dcea0cc3f..ac88cdc196 100644 --- a/packages/@uppy/core/src/_common.scss +++ b/packages/@uppy/core/src/_common.scss @@ -127,7 +127,7 @@ background-color: $blue; border-radius: 4px; - &:hover { + &:not(:disabled):hover { background-color: darken($blue, 10%); } @@ -145,6 +145,10 @@ @include blue-border-focus--dark; } + + &.uppy-c-btn--disabled { + background-color: rgb(142, 178, 219); + } } .uppy-c-btn-link { diff --git a/packages/@uppy/facebook/src/Facebook.tsx b/packages/@uppy/facebook/src/Facebook.tsx index f81c66c595..b1b8158073 100644 --- a/packages/@uppy/facebook/src/Facebook.tsx +++ b/packages/@uppy/facebook/src/Facebook.tsx @@ -103,19 +103,19 @@ export default class Facebook extends UIPlugin< } render(state: unknown): ComponentChild { - const viewOptions: { - viewType?: string - showFilter?: boolean - showTitles?: boolean - } = {} - 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) } } diff --git a/packages/@uppy/google-drive/src/DriveProviderViews.ts b/packages/@uppy/google-drive/src/DriveProviderViews.ts index 7ccbe80920..fe0bfb8e3a 100644 --- a/packages/@uppy/google-drive/src/DriveProviderViews.ts +++ b/packages/@uppy/google-drive/src/DriveProviderViews.ts @@ -1,18 +1,22 @@ +import type { + PartialTreeFile, + PartialTreeFolderNode, +} from '@uppy/core/lib/Uppy' import { ProviderViews } from '@uppy/provider-views' -import type { CompanionFile } from '@uppy/utils/lib/CompanionFile' import type { Body, Meta } from '@uppy/utils/lib/UppyFile' export default class DriveProviderViews< M extends Meta, B extends Body, > extends ProviderViews { - toggleCheckbox(e: Event, file: CompanionFile): void { - e.stopPropagation() - e.preventDefault() - - // Shared Drives aren't selectable; for all else, defer to the base ProviderView. - if (!file.custom!.isSharedDrive) { - super.toggleCheckbox(e, file) + toggleCheckbox( + item: PartialTreeFolderNode | PartialTreeFile, + isShiftKeyPressed: boolean, + ): void { + // We don't allow to check team drives; but we leave the checkboxes visible to show the 'partial' state + // (For a full explanation, see https://github.com/transloadit/uppy/issues/5232) + if (!item.data.custom?.isSharedDrive) { + super.toggleCheckbox(item, isShiftKeyPressed) } } } diff --git a/packages/@uppy/google-photos/src/GooglePhotos.tsx b/packages/@uppy/google-photos/src/GooglePhotos.tsx index 85c45f37a6..81df409d6c 100644 --- a/packages/@uppy/google-photos/src/GooglePhotos.tsx +++ b/packages/@uppy/google-photos/src/GooglePhotos.tsx @@ -114,10 +114,13 @@ export default class GooglePhotos< } render(state: unknown): ComponentChild { - 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/package.json b/packages/@uppy/provider-views/package.json index 964dbe22b2..e243b4b0e3 100644 --- a/packages/@uppy/provider-views/package.json +++ b/packages/@uppy/provider-views/package.json @@ -25,6 +25,9 @@ "p-queue": "^8.0.0", "preact": "^10.5.13" }, + "devDependencies": { + "vitest": "^1.6.0" + }, "peerDependencies": { "@uppy/core": "workspace:^" }, diff --git a/packages/@uppy/provider-views/src/Breadcrumbs.tsx b/packages/@uppy/provider-views/src/Breadcrumbs.tsx index 0312d42ec9..9e61083992 100644 --- a/packages/@uppy/provider-views/src/Breadcrumbs.tsx +++ b/packages/@uppy/provider-views/src/Breadcrumbs.tsx @@ -1,53 +1,35 @@ -import type { UnknownProviderPluginState } from '@uppy/core/lib/Uppy' +import type { PartialTreeFolder } from '@uppy/core/lib/Uppy' import { h, Fragment } from 'preact' import type { Body, Meta } from '@uppy/utils/lib/UppyFile' import type ProviderView from './ProviderView/index.js' -type BreadcrumbProps = { - getFolder: () => void - title: string - isLast: boolean -} - -const Breadcrumb = (props: BreadcrumbProps) => { - const { getFolder, title, isLast } = props - - return ( - - - {!isLast ? ' / ' : ''} - - ) -} - type BreadcrumbsProps = { - getFolder: ProviderView['getFolder'] + openFolder: ProviderView['openFolder'] title: string breadcrumbsIcon: h.JSX.Element - breadcrumbs: UnknownProviderPluginState['breadcrumbs'] + breadcrumbs: PartialTreeFolder[] } export default function Breadcrumbs( props: BreadcrumbsProps, -) { - const { getFolder, title, breadcrumbsIcon, breadcrumbs } = props +): h.JSX.Element { + const { openFolder, title, breadcrumbsIcon, breadcrumbs } = props return (
{breadcrumbsIcon}
- {breadcrumbs.map((directory, i) => ( - getFolder(directory.requestPath, directory.name)} - title={i === 0 ? title : (directory.name as string)} - isLast={i + 1 === breadcrumbs.length} - /> + {breadcrumbs.map((folder, index) => ( + + + {breadcrumbs.length === index + 1 ? '' : ' / '} + ))}
) diff --git a/packages/@uppy/provider-views/src/Browser.tsx b/packages/@uppy/provider-views/src/Browser.tsx index c1ccf61b63..62e494a5cd 100644 --- a/packages/@uppy/provider-views/src/Browser.tsx +++ b/packages/@uppy/provider-views/src/Browser.tsx @@ -1,273 +1,120 @@ -/* eslint-disable react/require-default-props */ import { h } from 'preact' -import classNames from 'classnames' -import remoteFileObjToLocal from '@uppy/utils/lib/remoteFileObjToLocal' -import { useMemo } from 'preact/hooks' // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore untyped import VirtualList from '@uppy/utils/lib/VirtualList' -import type { CompanionFile } from '@uppy/utils/lib/CompanionFile' -import type { Body, Meta, UppyFile } from '@uppy/utils/lib/UppyFile' +import type { Body, Meta } from '@uppy/utils/lib/UppyFile' import type { I18n } from '@uppy/utils/lib/Translator' -import type Uppy from '@uppy/core' -import SearchFilterInput from './SearchFilterInput.tsx' -import FooterActions from './FooterActions.tsx' +import type { + PartialTreeFile, + PartialTreeFolderNode, +} from '@uppy/core/lib/Uppy.ts' +import { useEffect, useState } from 'preact/hooks' import Item from './Item/index.tsx' - -const VIRTUAL_SHARED_DIR = 'shared-with-me' - -type ListItemProps = { - currentSelection: any[] - uppyFiles: UppyFile[] - viewType: string - isChecked: (file: any) => boolean - toggleCheckbox: (event: Event, file: CompanionFile) => void - recordShiftKeyPress: (event: KeyboardEvent | MouseEvent) => void - showTitles: boolean - i18n: I18n - validateRestrictions: Uppy['validateRestrictions'] - getNextFolder?: (folder: any) => void - f: CompanionFile -} - -function ListItem(props: ListItemProps) { - 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: Event) => toggleCheckbox(event, f), - recordShiftKeyPress, - type: 'folder', - // TODO: when was this supposed to be true? - isDisabled: false, - isCheckboxDisabled: f.id === VIRTUAL_SHARED_DIR, - // getNextFolder always exists when f.isFolder is true - 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: Event) => toggleCheckbox(event, f), - isCheckboxDisabled: false, - recordShiftKeyPress, - showTitles, - viewType, - i18n, - type: 'file', - isDisabled: Boolean(restrictionError) && !isChecked(f), - restrictionError, - }) -} +import ProviderView from './ProviderView/ProviderView.tsx' type BrowserProps = { - currentSelection: any[] - folders: CompanionFile[] - files: CompanionFile[] - uppyFiles: UppyFile[] + displayedPartialTree: (PartialTreeFile | PartialTreeFolderNode)[] viewType: string - headerComponent?: h.JSX.Element - showBreadcrumbs: boolean - isChecked: (file: any) => boolean - toggleCheckbox: (event: Event, file: CompanionFile) => void - recordShiftKeyPress: (event: KeyboardEvent | MouseEvent) => void - handleScroll: (event: Event) => Promise + toggleCheckbox: ProviderView['toggleCheckbox'] + handleScroll: ProviderView['handleScroll'] showTitles: boolean i18n: I18n - validateRestrictions: Uppy['validateRestrictions'] isLoading: boolean | string - showSearchFilter: boolean - search: (query: string) => void - searchTerm?: string | null - clearSearch: () => void - searchOnInput: boolean - searchInputLabel: string - clearSearchLabel: string - getNextFolder?: (folder: any) => void - cancel: () => void - done: () => void + openFolder: ProviderView['openFolder'] noResultsLabel: string - virtualList?: boolean + virtualList: boolean } function Browser(props: BrowserProps) { 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 [isShiftKeyPressed, setIsShiftKeyPressed] = useState(false) + + // This records whether the user is holding the SHIFT key this very moment. + // Typically, this is implemented using `onClick((e) => e.shiftKey)` - + // however we can't use that, because for accessibility reasons + // we're using html tags that don't support `e.shiftKey` property (see #3768). + useEffect(() => { + const handleKeyUp = (e: KeyboardEvent) => { + if (e.key === 'Shift') setIsShiftKeyPressed(false) + } + const handleKeyDown = (e: KeyboardEvent) => { + 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 ( +
+ {i18n('loading')} +
+ ) + } - const rows = useMemo(() => [...folders, ...files], [folders, files]) + if (displayedPartialTree.length === 0) { + return
{noResultsLabel}
+ } - return ( -
- {headerComponent && ( -
-
- {headerComponent} -
-
- )} + const renderItem = (item: PartialTreeFile | PartialTreeFolderNode) => ( + { + event.stopPropagation() + event.preventDefault() + // Prevent shift-clicking from highlighting file names + // (https://stackoverflow.com/a/1527797/3192470) + document.getSelection()?.removeAllRanges() + toggleCheckbox(item, isShiftKeyPressed) + }} + showTitles={showTitles} + i18n={i18n} + openFolder={openFolder} + file={item} + /> + ) - {showSearchFilter && ( -
- +
    + -
- )} - - {(() => { - if (isLoading) { - return ( -
- - {typeof isLoading === 'string' ? isLoading : i18n('loading')} - -
- ) - } - - if (!folders.length && !files.length) { - return
{noResultsLabel}
- } - - if (virtualList) { - return ( -
-
    - ( - - )} - rowHeight={31} - /> -
-
- ) - } - - return ( -
-
    not focusable for firefox - tabIndex={-1} - > - {rows.map((f) => ( - - ))} -
-
- ) - })()} - - {selected > 0 && ( - - )} + +
+ ) + } + return ( +
+
    not focusable for firefox + tabIndex={-1} + > + {displayedPartialTree.map(renderItem)} +
) } diff --git a/packages/@uppy/provider-views/src/CloseWrapper.ts b/packages/@uppy/provider-views/src/CloseWrapper.ts deleted file mode 100644 index 14502e9b55..0000000000 --- a/packages/@uppy/provider-views/src/CloseWrapper.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Component, toChildArray } from 'preact' - -export default class CloseWrapper extends Component<{ onUnmount: () => void }> { - componentWillUnmount(): void { - const { onUnmount } = this.props - onUnmount() - } - - render(): ReturnType[0] { - const { children } = this.props - return toChildArray(children)[0] - } -} diff --git a/packages/@uppy/provider-views/src/FooterActions.tsx b/packages/@uppy/provider-views/src/FooterActions.tsx index 9e8bb49d4d..b297d5424f 100644 --- a/packages/@uppy/provider-views/src/FooterActions.tsx +++ b/packages/@uppy/provider-views/src/FooterActions.tsx @@ -1,35 +1,69 @@ import { h } from 'preact' import type { I18n } from '@uppy/utils/lib/Translator' +import type { Meta, Body } from '@uppy/utils/lib/UppyFile' +import classNames from 'classnames' +import type { PartialTree } from '@uppy/core/lib/Uppy' +import { useMemo } from 'preact/hooks' +import getNumberOfSelectedFiles from './utils/PartialTreeUtils/getNumberOfSelectedFiles.ts' +import ProviderView from './ProviderView/ProviderView.tsx' -export default function FooterActions({ - cancel, - done, +export default function FooterActions({ + cancelSelection, + donePicking, i18n, - selected, + partialTree, + validateAggregateRestrictions, }: { - cancel: () => void - done: () => void + cancelSelection: ProviderView['cancelSelection'] + donePicking: ProviderView['donePicking'] i18n: I18n - selected: number + partialTree: PartialTree + validateAggregateRestrictions: ProviderView< + M, + B + >['validateAggregateRestrictions'] }) { + const aggregateRestrictionError = useMemo(() => { + return validateAggregateRestrictions(partialTree) + }, [partialTree, validateAggregateRestrictions]) + + const nOfSelectedFiles = useMemo(() => { + return getNumberOfSelectedFiles(partialTree) + }, [partialTree]) + + if (nOfSelectedFiles === 0) { + return null + } + return (
- - +
+ + +
+ + {aggregateRestrictionError && ( +
+ {aggregateRestrictionError} +
+ )}
) } diff --git a/packages/@uppy/provider-views/src/Item/components/GridItem.tsx b/packages/@uppy/provider-views/src/Item/components/GridItem.tsx index 766e4d5508..9dd3cfb1d1 100644 --- a/packages/@uppy/provider-views/src/Item/components/GridItem.tsx +++ b/packages/@uppy/provider-views/src/Item/components/GridItem.tsx @@ -1,72 +1,51 @@ -/* eslint-disable react/require-default-props */ -import { h, type ComponentChildren } from 'preact' -import classNames from 'classnames' -import type { RestrictionError } from '@uppy/core/lib/Restricter' -import type { Body, Meta } from '@uppy/utils/lib/UppyFile' +import { h } from 'preact' +import type { + PartialTreeFile, + PartialTreeFolderNode, +} from '@uppy/core/lib/Uppy' +import ItemIcon from './ItemIcon.tsx' -type GridItemProps = { +type GridItemProps = { + file: PartialTreeFile | PartialTreeFolderNode + toggleCheckbox: (event: Event) => void className: string isDisabled: boolean - restrictionError?: RestrictionError | null - isChecked: boolean - title?: string - itemIconEl: any - showTitles?: boolean - toggleCheckbox: (event: Event) => void - recordShiftKeyPress: (event: KeyboardEvent) => void - id: string - children?: ComponentChildren + restrictionError: string | null + showTitles: boolean + children?: h.JSX.Element | null } -function GridItem( - props: GridItemProps, -): h.JSX.Element { - const { - 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 }, - ) - +function GridItem({ + file, + toggleCheckbox, + className, + isDisabled, + restrictionError, + showTitles, + children = null, +}: GridItemProps): h.JSX.Element { return (
  • diff --git a/packages/@uppy/provider-views/src/Item/components/ListItem.tsx b/packages/@uppy/provider-views/src/Item/components/ListItem.tsx index 38e95eeb16..11ccfb47b3 100644 --- a/packages/@uppy/provider-views/src/Item/components/ListItem.tsx +++ b/packages/@uppy/provider-views/src/Item/components/ListItem.tsx @@ -1,7 +1,10 @@ -/* eslint-disable react/require-default-props */ -import type { RestrictionError } from '@uppy/core/lib/Restricter' -import type { Body, Meta } from '@uppy/utils/lib/UppyFile' +import type { + PartialTreeFile, + PartialTreeFolderNode, + PartialTreeId, +} from '@uppy/core/lib/Uppy' import { h } from 'preact' +import ItemIcon from './ItemIcon.tsx' // if folder: // + checkbox (selects all files from folder) @@ -9,96 +12,79 @@ import { h } from 'preact' // if file: // + checkbox (selects file) // + file name (selects file) - -type ListItemProps = { +type ListItemProps = { + file: PartialTreeFile | PartialTreeFolderNode + openFolder: (folderId: PartialTreeId) => void + toggleCheckbox: (event: Event) => void className: string isDisabled: boolean - restrictionError?: RestrictionError | null - isCheckboxDisabled: boolean - isChecked: boolean - toggleCheckbox: (event: Event) => void - recordShiftKeyPress: (event: KeyboardEvent | MouseEvent) => void - type: string - id: string - itemIconEl: any - title: string - handleFolderClick?: () => void + restrictionError: string | null showTitles: boolean i18n: any } -export default function ListItem( - props: ListItemProps, -): h.JSX.Element { - const { - className, - isDisabled, - restrictionError, - isCheckboxDisabled, - isChecked, - toggleCheckbox, - recordShiftKeyPress, - type, - id, - itemIconEl, - title, - handleFolderClick, - showTitles, - i18n, - } = props - +export default function ListItem({ + file, + openFolder, + className, + isDisabled, + restrictionError, + toggleCheckbox, + showTitles, + i18n, +}: ListItemProps): h.JSX.Element { return (
  • - {!isCheckboxDisabled ? - - name="listitem" - id={id} - checked={isChecked} - aria-label={ - type === 'file' ? null : ( - i18n('allFilesFromFolderNamed', { name: title }) - ) - } - disabled={isDisabled} - data-uppy-super-focusable - /> - : null} + + 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 + /> { - type === 'file' ? - // label for a checkbox - + file.data.isFolder ? // button to open a folder - : + // label for a checkbox + : }
  • diff --git a/packages/@uppy/provider-views/src/Item/index.tsx b/packages/@uppy/provider-views/src/Item/index.tsx index e127550b36..8fbf88cbd5 100644 --- a/packages/@uppy/provider-views/src/Item/index.tsx +++ b/packages/@uppy/provider-views/src/Item/index.tsx @@ -1,83 +1,66 @@ -/* eslint-disable react/require-default-props */ +/* eslint-disable react/jsx-props-no-spreading */ import { h } from 'preact' import classNames from 'classnames' import type { I18n } from '@uppy/utils/lib/Translator' -import type { CompanionFile } from '@uppy/utils/lib/CompanionFile' -import type { RestrictionError } from '@uppy/core/lib/Restricter' -import type { Meta, Body } from '@uppy/utils/lib/UppyFile' -import ItemIcon from './components/ItemIcon.tsx' +import type { + PartialTreeFile, + PartialTreeFolderNode, + PartialTreeId, +} from '@uppy/core/lib/Uppy.ts' import GridItem from './components/GridItem.tsx' import ListItem from './components/ListItem.tsx' -type ItemProps = { - showTitles: boolean - i18n: I18n - id: string - title: string +type ItemProps = { + file: PartialTreeFile | PartialTreeFolderNode + openFolder: (folderId: PartialTreeId) => void toggleCheckbox: (event: Event) => void - recordShiftKeyPress: (event: KeyboardEvent | MouseEvent) => void - handleFolderClick?: () => void - restrictionError?: RestrictionError | null - isCheckboxDisabled: boolean - type: 'folder' | 'file' - author?: CompanionFile['author'] - getItemIcon: () => string - isChecked: boolean - isDisabled: boolean viewType: string + showTitles: boolean + i18n: I18n } -export default function Item( - props: ItemProps, -): h.JSX.Element { - const { author, getItemIcon, isChecked, isDisabled, viewType } = props - const itemIconString = getItemIcon() +export default function Item(props: ItemProps): h.JSX.Element { + const { viewType, toggleCheckbox, showTitles, i18n, openFolder, file } = props + + const restrictionError = file.type === 'folder' ? null : file.restrictionError + const isDisabled = !!restrictionError && file.status !== 'checked' - const className = classNames( - 'uppy-ProviderBrowserItem', - { 'uppy-ProviderBrowserItem--selected': isChecked }, - { 'uppy-ProviderBrowserItem--disabled': isDisabled }, - { 'uppy-ProviderBrowserItem--noPreview': itemIconString === 'video' }, - ) + const ourProps = { + file, + openFolder, + toggleCheckbox, - const itemIconEl = + 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 ( - - // eslint-disable-next-line react/jsx-props-no-spreading - {...props} - className={className} - itemIconEl={itemIconEl} - /> - ) + return case 'list': - return ( - - // eslint-disable-next-line react/jsx-props-no-spreading - {...props} - className={className} - itemIconEl={itemIconEl} - /> - ) + return case 'unsplash': return ( - - // eslint-disable-next-line react/jsx-props-no-spreading - {...props} - className={className} - itemIconEl={itemIconEl} - > + - {author!.name} + {file.data.author!.name} ) diff --git a/packages/@uppy/provider-views/src/ProviderView/AuthView.tsx b/packages/@uppy/provider-views/src/ProviderView/AuthView.tsx index 099e46a82c..5f04b9a4f1 100644 --- a/packages/@uppy/provider-views/src/ProviderView/AuthView.tsx +++ b/packages/@uppy/provider-views/src/ProviderView/AuthView.tsx @@ -2,7 +2,7 @@ import { h } from 'preact' import { useCallback } from 'preact/hooks' import type { Body, Meta } from '@uppy/utils/lib/UppyFile' import type Translator from '@uppy/utils/lib/Translator' -import type { ProviderViewOptions } from './ProviderView.js' +import type { Opts } from './ProviderView.js' import type ProviderViews from './ProviderView.js' type AuthViewProps = { @@ -11,7 +11,7 @@ type AuthViewProps = { pluginIcon: () => h.JSX.Element i18n: Translator['translateArray'] handleAuth: ProviderViews['handleAuth'] - renderForm?: ProviderViewOptions['renderAuthForm'] + renderForm?: Opts['renderAuthForm'] } function GoogleIcon() { diff --git a/packages/@uppy/provider-views/src/ProviderView/Header.tsx b/packages/@uppy/provider-views/src/ProviderView/Header.tsx index 4a2e6efa5c..2bdb4255c1 100644 --- a/packages/@uppy/provider-views/src/ProviderView/Header.tsx +++ b/packages/@uppy/provider-views/src/ProviderView/Header.tsx @@ -1,20 +1,21 @@ /* eslint-disable react/destructuring-assignment */ -import { h, Fragment } from 'preact' +import { h } from 'preact' import type { I18n } from '@uppy/utils/lib/Translator' import type { Body, Meta } from '@uppy/utils/lib/UppyFile' -import type { UnknownProviderPluginState } from '@uppy/core/lib/Uppy' +import type { PartialTreeFolder } from '@uppy/core/lib/Uppy.ts' +import classNames from 'classnames' import User from './User.tsx' import Breadcrumbs from '../Breadcrumbs.tsx' import type ProviderView from './ProviderView.js' type HeaderProps = { showBreadcrumbs: boolean - getFolder: ProviderView['getFolder'] - breadcrumbs: UnknownProviderPluginState['breadcrumbs'] + openFolder: ProviderView['openFolder'] + breadcrumbs: PartialTreeFolder[] pluginIcon: () => h.JSX.Element title: string logout: () => void - username: string | undefined + username: string | null i18n: I18n } @@ -22,16 +23,27 @@ export default function Header( props: HeaderProps, ) { return ( - - {props.showBreadcrumbs && ( - +
    + {props.showBreadcrumbs && ( + + )} + - )} - - +
    + ) } diff --git a/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx b/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx index edacb56ef2..629808a9ab 100644 --- a/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx +++ b/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx @@ -1,42 +1,37 @@ import { h } from 'preact' -import PQueue from 'p-queue' - -import { getSafeFileId } from '@uppy/utils/lib/generateFileID' - import type { UnknownProviderPlugin, + PartialTreeFolder, + PartialTreeFolderNode, + PartialTreeFile, UnknownProviderPluginState, - Uppy, + PartialTreeId, + PartialTree, } from '@uppy/core/lib/Uppy.ts' import type { Body, Meta } from '@uppy/utils/lib/UppyFile' -import type { CompanionFile } from '@uppy/utils/lib/CompanionFile' +import type { CompanionFile } from '@uppy/utils/lib/CompanionFile.ts' import type Translator from '@uppy/utils/lib/Translator' -import type { DefinePluginOpts } from '@uppy/core/lib/BasePlugin' +import classNames from 'classnames' +import type { ValidateableFile } from '@uppy/core/lib/Restricter.ts' +import remoteFileObjToLocal from '@uppy/utils/lib/remoteFileObjToLocal' import AuthView from './AuthView.tsx' import Header from './Header.tsx' import Browser from '../Browser.tsx' -import CloseWrapper from '../CloseWrapper.ts' -import View, { type ViewOptions } from '../View.ts' // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore We don't want TS to generate types for the package.json import packageJson from '../../package.json' - -function formatBreadcrumbs( - breadcrumbs: UnknownProviderPluginState['breadcrumbs'], -): string { - return breadcrumbs - .slice(1) - .map((directory) => directory.name) - .join('/') -} - -function prependPath(path: string | undefined, component: string): string { - if (!path) return component - return `${path}/${component}` -} - -export function defaultPickerIcon() { +import PartialTreeUtils from '../utils/PartialTreeUtils/index.ts' +import shouldHandleScroll from '../utils/shouldHandleScroll.ts' +import handleError from '../utils/handleError.ts' +import getClickedRange from '../utils/getClickedRange.ts' +import SearchInput from '../SearchInput.tsx' +import FooterActions from '../FooterActions.tsx' +import addFiles from '../utils/addFiles.ts' +import getCheckedFilesWithPaths from '../utils/PartialTreeUtils/getCheckedFilesWithPaths.ts' +import getBreadcrumbs from '../utils/PartialTreeUtils/getBreadcrumbs.ts' + +export function defaultPickerIcon(): h.JSX.Element { return (
    + + showBreadcrumbs={opts.showBreadcrumbs} + openFolder={this.openFolder} + breadcrumbs={breadcrumbs} + pluginIcon={pluginIcon} + title={this.plugin.title} + logout={this.logout} + username={username} + i18n={i18n} + /> + + {opts.showFilter && ( + { + this.plugin.setPluginState({ searchString: s }) + }} + submitSearchString={() => {}} + inputLabel={i18n('filter')} + clearSearchLabel={i18n('resetFilter')} + wrapperClassName="uppy-ProviderBrowser-searchFilter" + inputClassName="uppy-ProviderBrowser-searchFilterInput" + /> + )} + + + 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} + /> + + +
    ) } } diff --git a/packages/@uppy/provider-views/src/ProviderView/User.tsx b/packages/@uppy/provider-views/src/ProviderView/User.tsx index e9ef6cdf9c..53dab79c53 100644 --- a/packages/@uppy/provider-views/src/ProviderView/User.tsx +++ b/packages/@uppy/provider-views/src/ProviderView/User.tsx @@ -3,15 +3,17 @@ import { h, Fragment } from 'preact' type UserProps = { i18n: (phrase: string) => string logout: () => void - username: string | undefined + username: string | null } export default function User({ i18n, logout, username }: UserProps) { return ( - - {username} - + {username && ( + + {username} + + )}