diff --git a/.eslintrc.js b/.eslintrc.js index 9e749f1436..74c9677e37 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -340,6 +340,7 @@ module.exports = { { files: [ '*.test.js', + '*.test.ts', 'test/endtoend/*.js', 'bin/**.js', ], diff --git a/packages/@uppy/core/src/Uppy.ts b/packages/@uppy/core/src/Uppy.ts index 7c5ac1a0e1..6b063d2245 100644 --- a/packages/@uppy/core/src/Uppy.ts +++ b/packages/@uppy/core/src/Uppy.ts @@ -64,7 +64,7 @@ export type UnknownPlugin< export type UnknownProviderPluginState = { authenticated: boolean | undefined breadcrumbs: { - requestPath: string + requestPath?: string name?: string id?: string }[] diff --git a/packages/@uppy/drag-drop/.npmignore b/packages/@uppy/drag-drop/.npmignore new file mode 100644 index 0000000000..6c816673f0 --- /dev/null +++ b/packages/@uppy/drag-drop/.npmignore @@ -0,0 +1 @@ +tsconfig.* diff --git a/packages/@uppy/drag-drop/src/DragDrop.jsx b/packages/@uppy/drag-drop/src/DragDrop.tsx similarity index 60% rename from packages/@uppy/drag-drop/src/DragDrop.jsx rename to packages/@uppy/drag-drop/src/DragDrop.tsx index a6909c425c..714e251a9d 100644 --- a/packages/@uppy/drag-drop/src/DragDrop.jsx +++ b/packages/@uppy/drag-drop/src/DragDrop.tsx @@ -1,55 +1,69 @@ -import { UIPlugin } from '@uppy/core' +import { UIPlugin, type Uppy } from '@uppy/core' +import type { DefinePluginOpts } from '@uppy/core/lib/BasePlugin.ts' +import type { UIPluginOptions } from '@uppy/core/lib/UIPlugin.ts' +import type { Body, Meta } from '@uppy/utils/lib/UppyFile' +import type { ChangeEvent } from 'preact/compat' import toArray from '@uppy/utils/lib/toArray' import isDragDropSupported from '@uppy/utils/lib/isDragDropSupported' import getDroppedFiles from '@uppy/utils/lib/getDroppedFiles' -import { h } from 'preact' +import { h, type ComponentChild } from 'preact' +// 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' -import locale from './locale.js' +import locale from './locale.ts' + +interface DragDropOptions extends UIPluginOptions { + inputName?: string + allowMultipleFiles?: boolean + width?: string | number + height?: string | number + note?: string + onDragOver?: (event: DragEvent) => void + onDragLeave?: (event: DragEvent) => void + onDrop?: (event: DragEvent) => void +} + +// Default options, must be kept in sync with @uppy/react/src/DragDrop.js. +const defaultOptions = { + inputName: 'files[]', + width: '100%', + height: '100%', +} satisfies Partial /** * Drag & Drop plugin * */ -export default class DragDrop extends UIPlugin { +export default class DragDrop extends UIPlugin< + DefinePluginOpts, + M, + B +> { static VERSION = packageJson.version - constructor (uppy, opts) { - super(uppy, opts) + // Check for browser dragDrop support + private isDragDropSupported = isDragDropSupported() + + private removeDragOverClassTimeout: ReturnType + + private fileInputRef: HTMLInputElement + + constructor(uppy: Uppy, opts?: DragDropOptions) { + super(uppy, { + ...defaultOptions, + ...opts, + }) this.type = 'acquirer' this.id = this.opts.id || 'DragDrop' this.title = 'Drag & Drop' this.defaultLocale = locale - // Default options, must be kept in sync with @uppy/react/src/DragDrop.js. - const defaultOpts = { - target: null, - inputName: 'files[]', - width: '100%', - height: '100%', - note: null, - } - - // Merge default options with the ones set by user - this.opts = { ...defaultOpts, ...opts } - this.i18nInit() - - // Check for browser dragDrop support - this.isDragDropSupported = isDragDropSupported() - this.removeDragOverClassTimeout = null - - // Bind `this` to class methods - this.onInputChange = this.onInputChange.bind(this) - this.handleDragOver = this.handleDragOver.bind(this) - this.handleDragLeave = this.handleDragLeave.bind(this) - this.handleDrop = this.handleDrop.bind(this) - this.addFiles = this.addFiles.bind(this) - this.render = this.render.bind(this) } - addFiles (files) { + private addFiles = (files: File[]) => { const descriptors = files.map((file) => ({ source: this.id, name: file.name, @@ -58,8 +72,8 @@ export default class DragDrop extends UIPlugin { meta: { // path of the file relative to the ancestor directory the user selected. // e.g. 'docs/Old Prague/airbnb.pdf' - relativePath: file.relativePath || null, - }, + relativePath: (file as any).relativePath || null, + } as any as M, })) try { @@ -69,8 +83,8 @@ export default class DragDrop extends UIPlugin { } } - onInputChange (event) { - const files = toArray(event.target.files) + private onInputChange = (event: ChangeEvent) => { + const files = toArray((event.target as HTMLInputElement).files!) if (files.length > 0) { this.uppy.log('[DragDrop] Files selected through input') this.addFiles(files) @@ -82,21 +96,22 @@ export default class DragDrop extends UIPlugin { // ___Why not use value="" on instead? // Because if we use that method of clearing the input, // Chrome will not trigger change if we drop the same file twice (Issue #768). + // @ts-expect-error TS freaks out, but this is fine // eslint-disable-next-line no-param-reassign event.target.value = null } - handleDragOver (event) { + private handleDragOver = (event: DragEvent) => { event.preventDefault() event.stopPropagation() // Check if the "type" of the datatransfer object includes files. If not, deny drop. - const { types } = event.dataTransfer - const hasFiles = types.some(type => type === 'Files') + const { types } = event.dataTransfer! + const hasFiles = types.some((type) => type === 'Files') const { allowNewUpload } = this.uppy.getState() if (!hasFiles || !allowNewUpload) { // eslint-disable-next-line no-param-reassign - event.dataTransfer.dropEffect = 'none' + event.dataTransfer!.dropEffect = 'none' clearTimeout(this.removeDragOverClassTimeout) return } @@ -106,7 +121,7 @@ export default class DragDrop extends UIPlugin { // https://github.com/transloadit/uppy/issues/1978) // // eslint-disable-next-line no-param-reassign - event.dataTransfer.dropEffect = 'copy' + event.dataTransfer!.dropEffect = 'copy' clearTimeout(this.removeDragOverClassTimeout) this.setPluginState({ isDraggingOver: true }) @@ -114,7 +129,7 @@ export default class DragDrop extends UIPlugin { this.opts.onDragOver?.(event) } - handleDragLeave (event) { + private handleDragLeave = (event: DragEvent) => { event.preventDefault() event.stopPropagation() @@ -128,7 +143,7 @@ export default class DragDrop extends UIPlugin { this.opts.onDragLeave?.(event) } - handleDrop = async (event) => { + private handleDrop = async (event: DragEvent) => { event.preventDefault() event.stopPropagation() clearTimeout(this.removeDragOverClassTimeout) @@ -136,12 +151,12 @@ export default class DragDrop extends UIPlugin { // Remove dragover class this.setPluginState({ isDraggingOver: false }) - const logDropError = (error) => { + const logDropError = (error: any) => { this.uppy.log(error, 'error') } // Add all dropped files - const files = await getDroppedFiles(event.dataTransfer, { logDropError }) + const files = await getDroppedFiles(event.dataTransfer!, { logDropError }) if (files.length > 0) { this.uppy.log('[DragDrop] Files dropped') this.addFiles(files) @@ -150,47 +165,57 @@ export default class DragDrop extends UIPlugin { this.opts.onDrop?.(event) } - renderHiddenFileInput () { + private renderHiddenFileInput() { const { restrictions } = this.uppy.opts return ( { this.fileInputRef = ref }} + ref={(ref) => { + this.fileInputRef = ref! + }} name={this.opts.inputName} multiple={restrictions.maxNumberOfFiles !== 1} + // @ts-expect-error We actually want to coerce the array to a string (or keep it as null/undefined) accept={restrictions.allowedFileTypes} onChange={this.onInputChange} /> ) } - static renderArrowSvg () { + private static renderArrowSvg() { return ( -
{this.i18nArray('dropHereOr', { - browse: {this.i18n('browse')}, + browse: ( + {this.i18n('browse')} + ) as any, })}
) } - renderNote () { - return ( - {this.opts.note} - ) + private renderNote() { + return {this.opts.note} } - render () { + render(): ComponentChild { const dragDropClass = `uppy-u-reset uppy-DragDrop-container ${this.isDragDropSupported ? 'uppy-DragDrop--isDragDropSupported' : ''} @@ -222,7 +247,7 @@ export default class DragDrop extends UIPlugin { ) } - install () { + install(): void { const { target } = this.opts this.setPluginState({ @@ -234,7 +259,7 @@ export default class DragDrop extends UIPlugin { } } - uninstall () { + uninstall(): void { this.unmount() } } diff --git a/packages/@uppy/drag-drop/src/index.js b/packages/@uppy/drag-drop/src/index.js deleted file mode 100644 index d56d03c499..0000000000 --- a/packages/@uppy/drag-drop/src/index.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from './DragDrop.jsx' diff --git a/packages/@uppy/drag-drop/src/index.ts b/packages/@uppy/drag-drop/src/index.ts new file mode 100644 index 0000000000..22c89b650c --- /dev/null +++ b/packages/@uppy/drag-drop/src/index.ts @@ -0,0 +1 @@ +export { default } from './DragDrop.tsx' diff --git a/packages/@uppy/drag-drop/src/locale.js b/packages/@uppy/drag-drop/src/locale.ts similarity index 100% rename from packages/@uppy/drag-drop/src/locale.js rename to packages/@uppy/drag-drop/src/locale.ts diff --git a/packages/@uppy/drag-drop/tsconfig.build.json b/packages/@uppy/drag-drop/tsconfig.build.json new file mode 100644 index 0000000000..1b0ca41093 --- /dev/null +++ b/packages/@uppy/drag-drop/tsconfig.build.json @@ -0,0 +1,25 @@ +{ + "extends": "../../../tsconfig.shared", + "compilerOptions": { + "noImplicitAny": false, + "outDir": "./lib", + "paths": { + "@uppy/utils/lib/*": ["../utils/src/*"], + "@uppy/core": ["../core/src/index.js"], + "@uppy/core/lib/*": ["../core/src/*"] + }, + "resolveJsonModule": false, + "rootDir": "./src", + "skipLibCheck": true + }, + "include": ["./src/**/*.*"], + "exclude": ["./src/**/*.test.ts"], + "references": [ + { + "path": "../utils/tsconfig.build.json" + }, + { + "path": "../core/tsconfig.build.json" + } + ] +} diff --git a/packages/@uppy/drag-drop/tsconfig.json b/packages/@uppy/drag-drop/tsconfig.json new file mode 100644 index 0000000000..a76c3b714a --- /dev/null +++ b/packages/@uppy/drag-drop/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../../tsconfig.shared", + "compilerOptions": { + "emitDeclarationOnly": false, + "noEmit": true, + "paths": { + "@uppy/utils/lib/*": ["../utils/src/*"], + "@uppy/core": ["../core/src/index.js"], + "@uppy/core/lib/*": ["../core/src/*"], + }, + }, + "include": ["./package.json", "./src/**/*.*"], + "references": [ + { + "path": "../utils/tsconfig.build.json", + }, + { + "path": "../core/tsconfig.build.json", + }, + ], +} diff --git a/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx b/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx index 72027716d3..5c6597916b 100644 --- a/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx +++ b/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx @@ -233,7 +233,7 @@ export default class ProviderView extends View< if (index !== -1) { // means we navigated back to a known directory (already in the stack), so cut the stack off there breadcrumbs = breadcrumbs.slice(0, index + 1) - } else if (requestPath) { + } else { // we have navigated into a new (unknown) folder, add it to the stack breadcrumbs = [...breadcrumbs, { requestPath, name }] } diff --git a/packages/@uppy/tus/src/index.ts b/packages/@uppy/tus/src/index.ts index 97adb68164..9f0072d799 100644 --- a/packages/@uppy/tus/src/index.ts +++ b/packages/@uppy/tus/src/index.ts @@ -36,6 +36,8 @@ type RestTusUploadOptions = Omit< 'onShouldRetry' | 'onBeforeRequest' | 'headers' > +export type TusDetailedError = tus.DetailedError + export interface TusOpts extends PluginOpts, RestTusUploadOptions { diff --git a/packages/@uppy/utils/src/RateLimitedQueue.ts b/packages/@uppy/utils/src/RateLimitedQueue.ts index cfe4d42a5c..975920e558 100644 --- a/packages/@uppy/utils/src/RateLimitedQueue.ts +++ b/packages/@uppy/utils/src/RateLimitedQueue.ts @@ -32,7 +32,7 @@ type QueueOptions = { export interface AbortablePromise extends Promise { abort(cause?: unknown): void - abortOn: typeof abortOn + abortOn: (...args: Parameters) => AbortablePromise } export type WrapPromiseFunctionType any> = ( @@ -227,7 +227,7 @@ export class RateLimitedQueue { outerPromise.abort = (cause) => { queuedRequest.abort(cause) } - outerPromise.abortOn = abortOn + outerPromise.abortOn = abortOn as any return outerPromise } diff --git a/packages/@uppy/webcam/src/CameraIcon.jsx b/packages/@uppy/webcam/src/CameraIcon.jsx deleted file mode 100644 index add8393463..0000000000 --- a/packages/@uppy/webcam/src/CameraIcon.jsx +++ /dev/null @@ -1,9 +0,0 @@ -import { h } from 'preact' - -export default () => { - return ( - - ) -} diff --git a/packages/@uppy/webcam/src/CameraIcon.tsx b/packages/@uppy/webcam/src/CameraIcon.tsx new file mode 100644 index 0000000000..09084a86b6 --- /dev/null +++ b/packages/@uppy/webcam/src/CameraIcon.tsx @@ -0,0 +1,19 @@ +import { h, type ComponentChild } from 'preact' + +export default function CameraIcon(): ComponentChild { + return ( + + ) +} diff --git a/packages/@uppy/webcam/src/CameraScreen.jsx b/packages/@uppy/webcam/src/CameraScreen.jsx deleted file mode 100644 index 479a67b9c8..0000000000 --- a/packages/@uppy/webcam/src/CameraScreen.jsx +++ /dev/null @@ -1,119 +0,0 @@ -/* eslint-disable jsx-a11y/media-has-caption */ -import { h, Component } from 'preact' -import SnapshotButton from './SnapshotButton.jsx' -import RecordButton from './RecordButton.jsx' -import RecordingLength from './RecordingLength.jsx' -import VideoSourceSelect from './VideoSourceSelect.jsx' -import SubmitButton from './SubmitButton.jsx' -import DiscardButton from './DiscardButton.jsx' - -function isModeAvailable (modes, mode) { - return modes.includes(mode) -} - -class CameraScreen extends Component { - componentDidMount () { - const { onFocus } = this.props - onFocus() - } - - componentWillUnmount () { - const { onStop } = this.props - onStop() - } - - render () { - const { - src, - recordedVideo, - recording, - modes, - supportsRecording, - videoSources, - showVideoSourceDropdown, - showRecordingLength, - onSubmit, - i18n, - mirror, - onSnapshot, - onStartRecording, - onStopRecording, - onDiscardRecordedVideo, - recordingLengthSeconds, - } = this.props - - const hasRecordedVideo = !!recordedVideo - const shouldShowRecordButton = !hasRecordedVideo && supportsRecording && ( - isModeAvailable(modes, 'video-only') - || isModeAvailable(modes, 'audio-only') - || isModeAvailable(modes, 'video-audio') - ) - const shouldShowSnapshotButton = !hasRecordedVideo && isModeAvailable(modes, 'picture') - const shouldShowRecordingLength = supportsRecording && showRecordingLength && !hasRecordedVideo - const shouldShowVideoSourceDropdown = showVideoSourceDropdown && videoSources && videoSources.length > 1 - - const videoProps = { - playsinline: true, - } - - if (recordedVideo) { - videoProps.muted = false - videoProps.controls = true - videoProps.src = recordedVideo - - // reset srcObject in dom. If not resetted, stream sticks in element - if (this.videoElement) { - this.videoElement.srcObject = undefined - } - } else { - videoProps.muted = true - videoProps.autoplay = true - videoProps.srcObject = src - } - - return ( -
-
-
-
-
- {shouldShowVideoSourceDropdown - ? VideoSourceSelect(this.props) - : null} -
-
- {shouldShowSnapshotButton && } - - {shouldShowRecordButton && ( - - )} - - {hasRecordedVideo && } - - {hasRecordedVideo && } -
- -
- {shouldShowRecordingLength && ( - - )} -
-
-
- ) - } -} - -export default CameraScreen diff --git a/packages/@uppy/webcam/src/CameraScreen.tsx b/packages/@uppy/webcam/src/CameraScreen.tsx new file mode 100644 index 0000000000..c7b7cf275e --- /dev/null +++ b/packages/@uppy/webcam/src/CameraScreen.tsx @@ -0,0 +1,164 @@ +/* eslint-disable jsx-a11y/media-has-caption */ +import type { I18n } from '@uppy/utils/lib/Translator' +import { h, Component, type ComponentChild } from 'preact' +import type { HTMLAttributes } from 'preact/compat' +import SnapshotButton from './SnapshotButton.tsx' +import RecordButton from './RecordButton.tsx' +import RecordingLength from './RecordingLength.tsx' +import VideoSourceSelect, { + type VideoSourceSelectProps, +} from './VideoSourceSelect.tsx' +import SubmitButton from './SubmitButton.tsx' +import DiscardButton from './DiscardButton.tsx' + +function isModeAvailable(modes: T[], mode: any): mode is T { + return modes.includes(mode) +} + +interface CameraScreenProps extends VideoSourceSelectProps { + onFocus: () => void + onStop: () => void + + src: MediaStream | null + recording: boolean + modes: string[] + supportsRecording: boolean + showVideoSourceDropdown: boolean + showRecordingLength: boolean + onSubmit: () => void + i18n: I18n + mirror: boolean + onSnapshot: () => void + onStartRecording: () => void + onStopRecording: () => void + onDiscardRecordedVideo: () => void + recordingLengthSeconds: number +} + +class CameraScreen extends Component { + private videoElement: HTMLVideoElement + + refs: any + + componentDidMount(): void { + const { onFocus } = this.props + onFocus() + } + + componentWillUnmount(): void { + const { onStop } = this.props + onStop() + } + + render(): ComponentChild { + const { + src, + // @ts-expect-error TODO: remove unused + recordedVideo, + recording, + modes, + supportsRecording, + videoSources, + showVideoSourceDropdown, + showRecordingLength, + onSubmit, + i18n, + mirror, + onSnapshot, + onStartRecording, + onStopRecording, + onDiscardRecordedVideo, + recordingLengthSeconds, + } = this.props + + const hasRecordedVideo = !!recordedVideo + const shouldShowRecordButton = + !hasRecordedVideo && + supportsRecording && + (isModeAvailable(modes, 'video-only') || + isModeAvailable(modes, 'audio-only') || + isModeAvailable(modes, 'video-audio')) + const shouldShowSnapshotButton = + !hasRecordedVideo && isModeAvailable(modes, 'picture') + const shouldShowRecordingLength = + supportsRecording && showRecordingLength && !hasRecordedVideo + const shouldShowVideoSourceDropdown = + showVideoSourceDropdown && videoSources && videoSources.length > 1 + + const videoProps: HTMLAttributes = { + playsInline: true, + } + + if (recordedVideo) { + videoProps.muted = false + videoProps.controls = true + videoProps.src = recordedVideo + + // reset srcObject in dom. If not resetted, stream sticks in element + if (this.videoElement) { + this.videoElement.srcObject = null + } + } else { + videoProps.muted = true + videoProps.autoPlay = true + // @ts-expect-error srcObject does not exist on