diff --git a/packages/@uppy/companion-client/src/Provider.ts b/packages/@uppy/companion-client/src/Provider.ts index 81bcb609d4..4ee9726d28 100644 --- a/packages/@uppy/companion-client/src/Provider.ts +++ b/packages/@uppy/companion-client/src/Provider.ts @@ -1,10 +1,12 @@ -import type { Uppy, BasePlugin } from '@uppy/core' -import type { Body, Meta, UppyFile } from '@uppy/utils/lib/UppyFile' +import type { Uppy } from '@uppy/core' +import type { Body, Meta } from '@uppy/utils/lib/UppyFile' import type { PluginOpts } from '@uppy/core/lib/BasePlugin.ts' -import RequestClient, { - authErrorStatusCode, - type RequestOptions, -} from './RequestClient.ts' +import type { + RequestOptions, + CompanionClientProvider, +} from '@uppy/utils/lib/CompanionClientProvider' +import type { UnknownProviderPlugin } from '@uppy/core/lib/Uppy.ts' +import RequestClient, { authErrorStatusCode } from './RequestClient.ts' import * as tokenStorage from './tokenStorage.ts' // TODO: remove deprecated options in next major release @@ -22,13 +24,6 @@ export interface Opts extends PluginOpts { provider: string } -interface ProviderPlugin - extends BasePlugin { - files: UppyFile[] - - storage: typeof tokenStorage -} - const getName = (id: string) => { return id .split('-') @@ -64,10 +59,10 @@ function isOriginAllowed( ) // allowing for trailing '/' } -export default class Provider< - M extends Meta, - B extends Body, -> extends RequestClient { +export default class Provider + extends RequestClient + implements CompanionClientProvider +{ #refreshingTokenPromise: Promise | undefined provider: string @@ -141,7 +136,10 @@ export default class Provider< } #getPlugin() { - const plugin = this.uppy.getPlugin(this.pluginId) as ProviderPlugin + const plugin = this.uppy.getPlugin(this.pluginId) as UnknownProviderPlugin< + M, + B + > if (plugin == null) throw new Error('Plugin was nullish') return plugin } @@ -375,23 +373,21 @@ export default class Provider< } } - list>( + list( directory: string | undefined, options: RequestOptions, ): Promise { return this.get(`${this.id}/list/${directory || ''}`, options) } - async logout>( - options: RequestOptions, - ): Promise { + async logout(options?: RequestOptions): Promise { const response = await this.get(`${this.id}/logout`, options) await this.removeAuthToken() return response } static initPlugin( - plugin: ProviderPlugin, // any because static methods cannot use class generics + plugin: UnknownProviderPlugin, // any because static methods cannot use class generics opts: Opts, defaultOpts: Record, ): void { diff --git a/packages/@uppy/companion-client/src/RequestClient.ts b/packages/@uppy/companion-client/src/RequestClient.ts index 1a4cf011d6..e7affb3cee 100644 --- a/packages/@uppy/companion-client/src/RequestClient.ts +++ b/packages/@uppy/companion-client/src/RequestClient.ts @@ -11,6 +11,7 @@ import getSocketHost from '@uppy/utils/lib/getSocketHost' import type Uppy from '@uppy/core' import type { UppyFile, Meta, Body } from '@uppy/utils/lib/UppyFile' +import type { RequestOptions } from '@uppy/utils/lib/CompanionClientProvider.ts' import AuthError from './AuthError.ts' // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore We don't want TS to generate types for the package.json @@ -28,13 +29,6 @@ export type Opts = { companionKeysParams?: Record } -export type RequestOptions = { - method?: string - data?: Record - skipPostResponse?: boolean - signal?: AbortSignal - qs?: Record -} type _RequestOptions = | boolean // TODO: remove this on the next major | RequestOptions diff --git a/packages/@uppy/companion-client/src/SearchProvider.ts b/packages/@uppy/companion-client/src/SearchProvider.ts index 9ad70db560..7a365a1808 100644 --- a/packages/@uppy/companion-client/src/SearchProvider.ts +++ b/packages/@uppy/companion-client/src/SearchProvider.ts @@ -2,6 +2,7 @@ import type { Body, Meta } from '@uppy/utils/lib/UppyFile.ts' import type { Uppy } from '@uppy/core' +import type { CompanionClientSearchProvider } from '@uppy/utils/lib/CompanionClientProvider' import RequestClient, { type Opts } from './RequestClient.ts' const getName = (id: string): string => { @@ -11,10 +12,10 @@ const getName = (id: string): string => { .join(' ') } -export default class SearchProvider< - M extends Meta, - B extends Body, -> extends RequestClient { +export default class SearchProvider + extends RequestClient + implements CompanionClientSearchProvider +{ provider: string id: string @@ -35,10 +36,7 @@ export default class SearchProvider< return `${this.hostname}/search/${this.id}/get/${id}` } - search>( - text: string, - queries?: string, - ): Promise { + search(text: string, queries?: string): Promise { return this.get( `search/${this.id}/list?q=${encodeURIComponent(text)}${ queries ? `&${queries}` : '' diff --git a/packages/@uppy/core/src/Uppy.ts b/packages/@uppy/core/src/Uppy.ts index c2d932e541..1d9f32f8ce 100644 --- a/packages/@uppy/core/src/Uppy.ts +++ b/packages/@uppy/core/src/Uppy.ts @@ -14,6 +14,11 @@ import getFileType from '@uppy/utils/lib/getFileType' import getFileNameAndExtension from '@uppy/utils/lib/getFileNameAndExtension' import { getSafeFileId } from '@uppy/utils/lib/generateFileID' import type { UppyFile, Meta, Body } from '@uppy/utils/lib/UppyFile' +import type { CompanionFile } from '@uppy/utils/lib/CompanionFile' +import type { + CompanionClientProvider, + CompanionClientSearchProvider, +} from '@uppy/utils/lib/CompanionClientProvider' import type { FileProgressNotStarted, FileProgressStarted, @@ -46,19 +51,83 @@ type FileRemoveReason = 'user' | 'cancel-all' type LogLevel = 'info' | 'warning' | 'error' | 'success' -export type UnknownPlugin = InstanceType< - typeof BasePlugin | typeof UIPlugin -> - -type UnknownProviderPlugin = UnknownPlugin< - M, - B -> & { - provider: { - logout: () => void +export type UnknownPlugin< + M extends Meta, + B extends Body, + PluginState extends Record = Record, +> = BasePlugin + +export type UnknownProviderPluginState = { + authenticated: boolean | undefined + breadcrumbs: { + requestPath: string + name: string + id?: string + }[] + didFirstRender: boolean + currentSelection: CompanionFile[] + filterInput: string + loading: boolean | string + folders: CompanionFile[] + files: CompanionFile[] + isSearchVisible: boolean +} +/* + * UnknownProviderPlugin can be any Companion plugin (such as Google Drive). + * As the plugins are passed around throughout Uppy we need a generic type for this. + * It may seems like duplication, but this type safe. Changing the type of `storage` + * will error in the `Provider` class of @uppy/companion-client and vice versa. + * + * Note that this is the *plugin* class, not a version of the `Provider` class. + * `Provider` does operate on Companion plugins with `uppy.getPlugin()`. + */ +export type UnknownProviderPlugin< + M extends Meta, + B extends Body, +> = UnknownPlugin & { + onFirstRender: () => void + title: string + files: UppyFile[] + icon: () => JSX.Element + provider: CompanionClientProvider + storage: { + getItem: (key: string) => Promise + setItem: (key: string, value: string) => Promise + removeItem: (key: string) => Promise } } +/* + * UnknownSearchProviderPlugin can be any search Companion plugin (such as Unsplash). + * As the plugins are passed around throughout Uppy we need a generic type for this. + * It may seems like duplication, but this type safe. Changing the type of `title` + * will error in the `SearchProvider` class of @uppy/companion-client and vice versa. + * + * Note that this is the *plugin* class, not a version of the `SearchProvider` class. + * `SearchProvider` does operate on Companion plugins with `uppy.getPlugin()`. + */ +export type UnknownSearchProviderPluginState = { + isInputMode?: boolean + searchTerm?: string | null +} & Pick< + UnknownProviderPluginState, + | 'loading' + | 'files' + | 'folders' + | 'currentSelection' + | 'filterInput' + | 'didFirstRender' +> +export type UnknownSearchProviderPlugin< + M extends Meta, + B extends Body, +> = UnknownPlugin & { + onFirstRender: () => void + title: string + icon: () => JSX.Element + provider: CompanionClientSearchProvider +} + // The user facing type for UppyFile used in uppy.addFile() and uppy.setOptions() export type MinimalRequiredUppyFile = Required< Pick, 'name' | 'data' | 'type' | 'source'> diff --git a/packages/@uppy/core/src/index.ts b/packages/@uppy/core/src/index.ts index d626cc56a1..ca8462d6c9 100644 --- a/packages/@uppy/core/src/index.ts +++ b/packages/@uppy/core/src/index.ts @@ -1,5 +1,12 @@ export { default } from './Uppy.ts' -export { default as Uppy, type UppyEventMap, type State } from './Uppy.ts' +export { + default as Uppy, + type UppyEventMap, + type State, + type UnknownPlugin, + type UnknownProviderPlugin, + type UnknownSearchProviderPlugin, +} from './Uppy.ts' export { default as UIPlugin } from './UIPlugin.ts' export { default as BasePlugin } from './BasePlugin.ts' export { debugLogger } from './loggers.ts' diff --git a/packages/@uppy/utils/package.json b/packages/@uppy/utils/package.json index ad351240ca..1755e38d56 100644 --- a/packages/@uppy/utils/package.json +++ b/packages/@uppy/utils/package.json @@ -66,6 +66,8 @@ "./lib/fileFilters": "./lib/fileFilters.js", "./lib/VirtualList": "./lib/VirtualList.js", "./lib/UppyFile": "./lib/UppyFile.js", + "./lib/CompanionFile": "./lib/CompanionFile.js", + "./lib/CompanionClientProvider": "./lib/CompanionClientProvider.js", "./lib/FileProgress": "./lib/FileProgress.js", "./src/microtip.scss": "./src/microtip.scss", "./lib/UserFacingApiError": "./lib/UserFacingApiError.js" diff --git a/packages/@uppy/utils/src/CompanionClientProvider.ts b/packages/@uppy/utils/src/CompanionClientProvider.ts new file mode 100644 index 0000000000..a7f2cba108 --- /dev/null +++ b/packages/@uppy/utils/src/CompanionClientProvider.ts @@ -0,0 +1,35 @@ +export type RequestOptions = { + method?: string + data?: Record + skipPostResponse?: boolean + signal?: AbortSignal + authFormData?: unknown + qs?: Record +} + +/** + * CompanionClientProvider is subset of the types of the `Provider` + * class from @uppy/companion-client. + * + * This is needed as the `Provider` class is passed around in Uppy and we + * need to have shared types for it. Although we are duplicating some types, + * this is still safe as `Provider implements CompanionClientProvider` + * so any changes here will error there and vice versa. + * + * TODO: remove this once companion-client and provider-views are merged into a single plugin. + */ +export interface CompanionClientProvider { + name: string + provider: string + login(options?: RequestOptions): Promise + logout(options?: RequestOptions): Promise + list( + directory: string | undefined, + options: RequestOptions, + ): Promise +} +export interface CompanionClientSearchProvider { + name: string + provider: string + search(text: string, queries?: string): Promise +} diff --git a/packages/@uppy/utils/src/CompanionFile.ts b/packages/@uppy/utils/src/CompanionFile.ts new file mode 100644 index 0000000000..44a2f1db9b --- /dev/null +++ b/packages/@uppy/utils/src/CompanionFile.ts @@ -0,0 +1,25 @@ +/** + * CompanionFile represents a file object returned by the Companion API. + */ +export type CompanionFile = { + id: string + name: string + /* + * Url to the thumbnail icon + */ + icon: string + type: string + mimeType: string + extension: string + size: number + isFolder: boolean + modifiedDate: string + thumbnail?: string + requestPath: string + relDirPath?: string + absDirPath?: string + author?: { + name?: string + url?: string + } +}