mirror of
https://github.com/transloadit/uppy.git
synced 2026-01-23 02:25:07 +00:00
fixes #6024. ### Problem - `getPlugin()` defaults to `UnknownPlugin`, so methods like `openModal` are not visible , since core is not aware of that plugin type ### Proposed change - Introduce a types-only registry in core: - `export interface PluginTypeRegistry<M extends Meta, B extends Body> {}` - Overload `getPlugin` to return a precise type when the id is a known key of the registry. - add `Dashboard` to PluginTypeRegistry through module augmentation: - `'Dashboard': Dashboard<M, B>`. - When a project imports from `@uppy/dashboard`, its module augmentation extends PluginTypeRegistry, adding the correct type into it - I've added Tests , kept them in a separate file so it's easier to review , once this approach gets approved I'll add them to `Uppy.test.ts` Once this PR gets a positive review I'll add this for other plugins , currently only added for `@uppy/dashboard` **Build with Local tarball can be checked here** https://stackblitz.com/~/github.com/qxprakash/uppy-type-test?file=type_test.ts
236 lines
6.6 KiB
TypeScript
236 lines
6.6 KiB
TypeScript
import {
|
|
type CompanionPluginOptions,
|
|
RequestClient,
|
|
} from '@uppy/companion-client'
|
|
import type { Body, Meta } from '@uppy/core'
|
|
import { UIPlugin, type Uppy } from '@uppy/core'
|
|
import type {
|
|
LocaleStrings,
|
|
MinimalRequiredUppyFile,
|
|
RemoteUppyFile,
|
|
} from '@uppy/utils'
|
|
import { toArray } from '@uppy/utils'
|
|
// biome-ignore lint/style/useImportType: h is not a type
|
|
import { type ComponentChild, h } from 'preact'
|
|
import packageJson from '../package.json' with { type: 'json' }
|
|
import locale from './locale.js'
|
|
import UrlUI from './UrlUI.js'
|
|
import forEachDroppedOrPastedUrl from './utils/forEachDroppedOrPastedUrl.js'
|
|
|
|
declare module '@uppy/core' {
|
|
export interface PluginTypeRegistry<M extends Meta, B extends Body> {
|
|
Url: Url<M, B>
|
|
}
|
|
}
|
|
|
|
function UrlIcon() {
|
|
return (
|
|
<svg
|
|
aria-hidden="true"
|
|
focusable="false"
|
|
width="32"
|
|
height="32"
|
|
viewBox="0 0 32 32"
|
|
>
|
|
<path
|
|
d="M23.637 15.312l-2.474 2.464a3.582 3.582 0 01-.577.491c-.907.657-1.897.986-2.968.986a4.925 4.925 0 01-3.959-1.971c-.248-.329-.164-.902.165-1.149.33-.247.907-.164 1.155.164 1.072 1.478 3.133 1.724 4.618.656a.642.642 0 00.33-.328l2.473-2.463c1.238-1.313 1.238-3.366-.082-4.597a3.348 3.348 0 00-4.618 0l-1.402 1.395a.799.799 0 01-1.154 0 .79.79 0 010-1.15l1.402-1.394a4.843 4.843 0 016.843 0c2.062 1.805 2.144 5.007.248 6.896zm-8.081 5.664l-1.402 1.395a3.348 3.348 0 01-4.618 0c-1.319-1.23-1.319-3.365-.082-4.596l2.475-2.464.328-.328c.743-.492 1.567-.739 2.475-.657.906.165 1.648.574 2.143 1.314.248.329.825.411 1.155.165.33-.248.412-.822.165-1.15-.825-1.068-1.98-1.724-3.216-1.888-1.238-.247-2.556.082-3.628.902l-.495.493-2.474 2.464c-1.897 1.969-1.814 5.09.083 6.977.99.904 2.226 1.396 3.463 1.396s2.473-.492 3.463-1.395l1.402-1.396a.79.79 0 000-1.15c-.33-.328-.908-.41-1.237-.082z"
|
|
fill="#FF753E"
|
|
fill-rule="nonzero"
|
|
/>
|
|
</svg>
|
|
)
|
|
}
|
|
|
|
function addProtocolToURL(url: string) {
|
|
const protocolRegex = /^[a-z0-9]+:\/\//
|
|
const defaultProtocol = 'http://'
|
|
if (protocolRegex.test(url)) {
|
|
return url
|
|
}
|
|
|
|
return defaultProtocol + url
|
|
}
|
|
|
|
function canHandleRootDrop(e: DragEvent) {
|
|
const items = toArray(e.dataTransfer!.items)
|
|
const urls = items.filter(
|
|
(item) => item.kind === 'string' && item.type === 'text/uri-list',
|
|
)
|
|
return urls.length > 0
|
|
}
|
|
|
|
function checkIfCorrectURL(url?: string) {
|
|
return url?.startsWith('http://') || url?.startsWith('https://')
|
|
}
|
|
|
|
function getFileNameFromUrl(url: string) {
|
|
const { pathname } = new URL(url)
|
|
return pathname.substring(pathname.lastIndexOf('/') + 1)
|
|
}
|
|
|
|
/*
|
|
* Response from the /url/meta Companion endpoint.
|
|
* Has to be kept in sync with `getURLMeta` in `companion/src/server/helpers/request.js`.
|
|
*/
|
|
type MetaResponse = {
|
|
name: string
|
|
type: string
|
|
size: number | null
|
|
statusCode: number
|
|
}
|
|
|
|
export type UrlOptions = CompanionPluginOptions & {
|
|
locale?: LocaleStrings<typeof locale>
|
|
}
|
|
|
|
export default class Url<M extends Meta, B extends Body> extends UIPlugin<
|
|
UrlOptions,
|
|
M,
|
|
B
|
|
> {
|
|
static VERSION = packageJson.version
|
|
|
|
static requestClientId = Url.name
|
|
|
|
icon: () => h.JSX.Element
|
|
|
|
hostname: string
|
|
|
|
client: RequestClient<M, B>
|
|
|
|
canHandleRootDrop!: typeof canHandleRootDrop
|
|
|
|
constructor(uppy: Uppy<M, B>, opts: UrlOptions) {
|
|
super(uppy, opts)
|
|
this.id = this.opts.id || 'Url'
|
|
this.type = 'acquirer'
|
|
this.icon = () => <UrlIcon />
|
|
|
|
// Set default options and locale
|
|
this.defaultLocale = locale
|
|
|
|
this.i18nInit()
|
|
this.title = this.i18n('pluginNameUrl')
|
|
|
|
this.hostname = this.opts.companionUrl
|
|
|
|
if (!this.hostname) {
|
|
throw new Error(
|
|
'Companion hostname is required, please consult https://uppy.io/docs/companion',
|
|
)
|
|
}
|
|
|
|
this.client = new RequestClient(uppy, {
|
|
pluginId: this.id,
|
|
provider: 'url',
|
|
companionUrl: this.opts.companionUrl,
|
|
companionHeaders: this.opts.companionHeaders,
|
|
companionCookiesRule: this.opts.companionCookiesRule,
|
|
})
|
|
|
|
this.uppy.registerRequestClient(Url.requestClientId, this.client)
|
|
}
|
|
|
|
private getMeta = (url: string): Promise<MetaResponse> => {
|
|
return this.client.post<MetaResponse>('url/meta', { url })
|
|
}
|
|
|
|
private addFile = async (
|
|
protocollessUrl: string,
|
|
optionalMeta?: M,
|
|
): Promise<string | undefined> => {
|
|
// Do not process local files
|
|
if (protocollessUrl.startsWith('blob')) {
|
|
return undefined
|
|
}
|
|
const url = addProtocolToURL(protocollessUrl)
|
|
if (!checkIfCorrectURL(url)) {
|
|
this.uppy.log(`[URL] Incorrect URL entered: ${url}`)
|
|
this.uppy.info(this.i18n('enterCorrectUrl'), 'error', 4000)
|
|
return undefined
|
|
}
|
|
|
|
this.uppy.log(`[URL] Adding file from dropped/pasted url: ${url}`)
|
|
|
|
try {
|
|
const meta = await this.getMeta(url)
|
|
|
|
const file: Omit<RemoteUppyFile<M, B>, 'name' | 'meta' | 'body'> &
|
|
Pick<MinimalRequiredUppyFile<M, B>, 'name' | 'meta'> = {
|
|
meta: optionalMeta,
|
|
source: this.id,
|
|
name: meta.name || getFileNameFromUrl(url),
|
|
type: meta.type,
|
|
data: {
|
|
size: meta.size,
|
|
},
|
|
isRemote: true,
|
|
// @ts-expect-error TODO: should this be removed? the types say it's not needed
|
|
body: {
|
|
url,
|
|
},
|
|
remote: {
|
|
companionUrl: this.opts.companionUrl,
|
|
url: `${this.hostname}/url/get`,
|
|
body: {
|
|
fileId: url,
|
|
url,
|
|
},
|
|
requestClientId: Url.requestClientId,
|
|
},
|
|
}
|
|
|
|
this.uppy.log('[Url] Adding remote file')
|
|
try {
|
|
return this.uppy.addFile(file)
|
|
} catch (err) {
|
|
if (!err.isRestriction) {
|
|
this.uppy.log(err)
|
|
}
|
|
return err
|
|
}
|
|
} catch (err) {
|
|
this.uppy.log(err)
|
|
this.uppy.info(
|
|
{
|
|
message: this.i18n('failedToFetch'),
|
|
details: err,
|
|
},
|
|
'error',
|
|
4000,
|
|
)
|
|
return err
|
|
}
|
|
}
|
|
|
|
private handleRootDrop = (e: DragEvent) => {
|
|
forEachDroppedOrPastedUrl(e.dataTransfer!, 'drop', (url) => {
|
|
this.addFile(url)
|
|
})
|
|
}
|
|
|
|
private handleRootPaste = (e: ClipboardEvent) => {
|
|
forEachDroppedOrPastedUrl(e.clipboardData!, 'paste', (url) => {
|
|
this.addFile(url)
|
|
})
|
|
}
|
|
|
|
render(): ComponentChild {
|
|
return <UrlUI i18n={this.i18n} addFile={this.addFile} />
|
|
}
|
|
|
|
install(): void {
|
|
const { target } = this.opts
|
|
if (target) {
|
|
this.mount(target, this)
|
|
}
|
|
}
|
|
|
|
uninstall(): void {
|
|
this.unmount()
|
|
}
|
|
}
|
|
|
|
// This is defined outside of the class body because it's not using `this`, but
|
|
// we still want it available on the prototype so the Dashboard can access it.
|
|
Url.prototype.canHandleRootDrop = canHandleRootDrop
|