Skip to content

Commit

Permalink
Dashboard - convert some files to typescript (#5367)
Browse files Browse the repository at this point in the history
Co-authored-by: Murderlon <[email protected]>
lakesare and Murderlon authored Oct 15, 2024

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
1 parent 9a4b8ef commit 922e7fb
Showing 5 changed files with 252 additions and 97 deletions.
2 changes: 1 addition & 1 deletion packages/@uppy/core/src/UIPlugin.ts
Original file line number Diff line number Diff line change
@@ -183,7 +183,7 @@ class UIPlugin<
// eslint-disable-next-line @typescript-eslint/no-unused-vars
state: Record<string, unknown>,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
container: HTMLElement,
container?: HTMLElement,
): any {
throw new Error(
'Extend the render method to add your plugin to a DOM element',
50 changes: 33 additions & 17 deletions packages/@uppy/dashboard/src/Dashboard.tsx
Original file line number Diff line number Diff line change
@@ -97,8 +97,8 @@ interface Target {
type: string
}

interface TargetWithRender extends Target {
icon: ComponentChild
export interface TargetWithRender extends Target {
icon: () => ComponentChild
render: () => ComponentChild
}

@@ -110,6 +110,12 @@ export interface DashboardState<M extends Meta, B extends Body> {
fileCardFor: string | null
showFileEditor: boolean
metaFields?: MetaField[] | ((file: UppyFile<M, B>) => MetaField[])
isHidden: boolean
isClosing: boolean
containerWidth: number
containerHeight: number
areInsidesReadyToBeVisible: boolean
isDraggingOver: boolean
[key: string]: unknown
}

@@ -146,7 +152,7 @@ interface DashboardMiscOptions<M extends Meta, B extends Body>
hideRetryButton?: boolean
hideUploadButton?: boolean
metaFields?: MetaField[] | ((file: UppyFile<M, B>) => MetaField[])
nativeCameraFacingMode?: ConstrainDOMString
nativeCameraFacingMode?: 'user' | 'environment' | ''
note?: string | null
onDragLeave?: (event: DragEvent) => void
onDragOver?: (event: DragEvent) => void
@@ -165,7 +171,7 @@ interface DashboardMiscOptions<M extends Meta, B extends Body>
thumbnailHeight?: number
thumbnailType?: string
thumbnailWidth?: number
trigger?: string | Element
trigger?: string | Element | null
waitForThumbnailsBeforeUpload?: boolean
}

@@ -178,9 +184,6 @@ export type DashboardOptions<
const defaultOptions = {
target: 'body',
metaFields: [],
inline: false as boolean,
width: 750,
height: 550,
thumbnailWidth: 280,
thumbnailType: 'image/jpeg',
waitForThumbnailsBeforeUpload: false,
@@ -193,31 +196,44 @@ const defaultOptions = {
hidePauseResumeButton: false,
hideProgressAfterFinish: false,
note: null,
closeModalOnClickOutside: false,
closeAfterFinish: false,
singleFileFullScreen: true,
disableStatusBar: false,
disableInformer: false,
disableThumbnailGenerator: false,
disablePageScrollWhenModalOpen: true,
animateOpenClose: true,
fileManagerSelectionType: 'files',
proudlyDisplayPoweredByUppy: true,
showSelectedFiles: true,
showRemoveButtonAfterComplete: false,
browserBackButtonClose: false,
showNativePhotoCameraButton: false,
showNativeVideoCameraButton: false,
theme: 'light',
autoOpen: null,
disabled: false,
disableLocalFiles: false,
nativeCameraFacingMode: '',
onDragLeave: () => {},
onDragOver: () => {},
onDrop: () => {},
plugins: [],

// Dynamic default options, they have to be defined in the constructor (because
// they require access to the `this` keyword), but we still want them to
// appear in the default options so TS knows they'll be defined.
doneButtonHandler: undefined as any,
onRequestCloseModal: null as any,

// defaultModalOptions
inline: false as boolean,
animateOpenClose: true,
browserBackButtonClose: false,
closeAfterFinish: false,
closeModalOnClickOutside: false,
disablePageScrollWhenModalOpen: true,
trigger: null,

// defaultInlineOptions
width: 750,
height: 550,
} satisfies Partial<DashboardOptions<any, any>>

/**
@@ -824,7 +840,7 @@ export default class Dashboard<M extends Meta, B extends Body> extends UIPlugin<

this.setPluginState({ isDraggingOver: true })

this.opts.onDragOver?.(event)
this.opts.onDragOver(event)
}

private handleDragLeave = (event: DragEvent) => {
@@ -833,7 +849,7 @@ export default class Dashboard<M extends Meta, B extends Body> extends UIPlugin<

this.setPluginState({ isDraggingOver: false })

this.opts.onDragLeave?.(event)
this.opts.onDragLeave(event)
}

private handleDrop = async (event: DragEvent) => {
@@ -872,7 +888,7 @@ export default class Dashboard<M extends Meta, B extends Body> extends UIPlugin<
this.addFiles(files)
}

this.opts.onDrop?.(event)
this.opts.onDrop(event)
}

private handleRequestThumbnail = (file: UppyFile<M, B>) => {
@@ -1260,7 +1276,7 @@ export default class Dashboard<M extends Meta, B extends Body> extends UIPlugin<
}

#addSpecifiedPluginsFromOptions = () => {
const plugins = this.opts.plugins || []
const { plugins } = this.opts

plugins.forEach((pluginID) => {
const plugin = this.uppy.getPlugin(pluginID)
@@ -1466,7 +1482,7 @@ export default class Dashboard<M extends Meta, B extends Body> extends UIPlugin<
if (thumbnail) this.uppy.removePlugin(thumbnail)
}

const plugins = this.opts.plugins || []
const { plugins } = this.opts
plugins.forEach((pluginID) => {
const plugin = this.uppy.getPlugin(pluginID)
if (plugin) (plugin as any).unmount()
108 changes: 67 additions & 41 deletions packages/@uppy/dashboard/src/components/AddFiles.tsx
Original file line number Diff line number Diff line change
@@ -1,34 +1,51 @@
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-nocheck Typing this file requires more work, skipping it to unblock the rest of the transition.

/* eslint-disable react/destructuring-assignment */
import { h, Component, Fragment, type ComponentChild } from 'preact'
import type { I18n } from '@uppy/utils/lib/Translator'
import type Translator from '@uppy/utils/lib/Translator'
import type { TargetedEvent } from 'preact/compat'
import type { DashboardState, TargetWithRender } from '../Dashboard'

interface AddFilesProps {
i18n: I18n
i18nArray: Translator['translateArray']
acquirers: TargetWithRender[]
handleInputChange: (event: TargetedEvent<HTMLInputElement, Event>) => void
maxNumberOfFiles: number | null
allowedFileTypes: string[] | null
showNativePhotoCameraButton: boolean
showNativeVideoCameraButton: boolean
nativeCameraFacingMode: 'user' | 'environment' | ''
showPanel: (id: string) => void
activePickerPanel: DashboardState<any, any>['activePickerPanel']
disableLocalFiles: boolean
fileManagerSelectionType: string
note: string | null
proudlyDisplayPoweredByUppy: boolean
}

type $TSFixMe = any

class AddFiles extends Component {
fileInput: $TSFixMe
class AddFiles extends Component<AddFilesProps> {
fileInput: HTMLInputElement | null = null

folderInput: $TSFixMe
folderInput: HTMLInputElement | null = null

mobilePhotoFileInput: $TSFixMe
mobilePhotoFileInput: HTMLInputElement | null = null

mobileVideoFileInput: $TSFixMe
mobileVideoFileInput: HTMLInputElement | null = null

private triggerFileInputClick = () => {
this.fileInput.click()
this.fileInput?.click()
}

private triggerFolderInputClick = () => {
this.folderInput.click()
this.folderInput?.click()
}

private triggerVideoCameraInputClick = () => {
this.mobileVideoFileInput.click()
this.mobileVideoFileInput?.click()
}

private triggerPhotoCameraInputClick = () => {
this.mobilePhotoFileInput.click()
this.mobilePhotoFileInput?.click()
}

private onFileInputChange = (
@@ -42,13 +59,17 @@ class AddFiles extends Component {
event.currentTarget.value = ''
}

private renderHiddenInput = (isFolder: $TSFixMe, refCallback: $TSFixMe) => {
private renderHiddenInput = (
isFolder: boolean,
refCallback: (ref: HTMLInputElement | null) => void,
) => {
return (
<input
className="uppy-Dashboard-input"
hidden
aria-hidden="true"
tabIndex={-1}
// @ts-expect-error default types don't yet know about the `webkitdirectory` property
webkitdirectory={isFolder}
type="file"
name="files[]"
@@ -61,9 +82,9 @@ class AddFiles extends Component {
}

private renderHiddenCameraInput = (
type: $TSFixMe,
nativeCameraFacingMode: $TSFixMe,
refCallback: $TSFixMe,
type: 'photo' | 'video',
nativeCameraFacingMode: 'user' | 'environment' | '',
refCallback: (ref: HTMLInputElement | null) => void,
) => {
const typeToAccept = { photo: 'image/*', video: 'video/*' }
const accept = typeToAccept[type]
@@ -193,7 +214,10 @@ class AddFiles extends Component {
)
}

private renderBrowseButton = (text: $TSFixMe, onClickFn: $TSFixMe) => {
private renderBrowseButton = (
text: string,
onClickFn: (event: Event) => void,
) => {
const numberOfAcquirers = this.props.acquirers.length
return (
<button
@@ -207,7 +231,7 @@ class AddFiles extends Component {
)
}

private renderDropPasteBrowseTagline = (numberOfAcquirers: $TSFixMe) => {
private renderDropPasteBrowseTagline = (numberOfAcquirers: number) => {
const browseFiles = this.renderBrowseButton(
this.props.i18n('browseFiles'),
this.triggerFileInputClick,
@@ -257,7 +281,7 @@ class AddFiles extends Component {
this.props.i18nArray('dropPasteImportFolders')
}

private renderAcquirer = (acquirer: $TSFixMe) => {
private renderAcquirer = (acquirer: TargetWithRender) => {
return (
<div
className="uppy-DashboardTab"
@@ -282,7 +306,7 @@ class AddFiles extends Component {
)
}

private renderAcquirers = (acquirers: $TSFixMe) => {
private renderAcquirers = (acquirers: TargetWithRender[]) => {
// Group last two buttons, so we don’t end up with
// just one button on a new line
const acquirersWithoutLastTwo = [...acquirers]
@@ -304,18 +328,22 @@ class AddFiles extends Component {
}

private renderSourcesList = (
acquirers: $TSFixMe,
disableLocalFiles: $TSFixMe,
acquirers: TargetWithRender[],
disableLocalFiles: boolean,
) => {
const { showNativePhotoCameraButton, showNativeVideoCameraButton } =
this.props

let list = []
type RenderListItem = { key: string; elements: ComponentChild }
let list: RenderListItem[] = []

const myDeviceKey = 'myDevice'

if (!disableLocalFiles)
list.push({ key: myDeviceKey, elements: this.renderMyDeviceAcquirer() })
list.push({
key: myDeviceKey,
elements: this.renderMyDeviceAcquirer(),
})
if (showNativePhotoCameraButton)
list.push({
key: 'nativePhotoCameraButton',
@@ -327,7 +355,7 @@ class AddFiles extends Component {
elements: this.renderVideoCamera(),
})
list.push(
...acquirers.map((acquirer: $TSFixMe) => ({
...acquirers.map((acquirer: TargetWithRender) => ({
key: acquirer.id,
elements: this.renderAcquirer(acquirer),
})),
@@ -342,28 +370,27 @@ class AddFiles extends Component {
const listWithoutLastTwo = [...list]
const lastTwo = listWithoutLastTwo.splice(list.length - 2, list.length)

const renderList = (l: $TSFixMe) =>
l.map(({ key, elements }: $TSFixMe) => (
<Fragment key={key}>{elements}</Fragment>
))

return (
<>
{this.renderDropPasteBrowseTagline(list.length)}

<div className="uppy-Dashboard-AddFiles-list" role="tablist">
{renderList(listWithoutLastTwo)}
{listWithoutLastTwo.map(({ key, elements }) => (
<Fragment key={key}>{elements}</Fragment>
))}

<span role="presentation" style={{ 'white-space': 'nowrap' }}>
{renderList(lastTwo)}
{lastTwo.map(({ key, elements }) => (
<Fragment key={key}>{elements}</Fragment>
))}
</span>
</div>
</>
)
}

private renderPoweredByUppy() {
const { i18nArray } = this.props as $TSFixMe
const { i18nArray } = this.props

const uppyBranding = (
<span>
@@ -408,25 +435,25 @@ class AddFiles extends Component {

return (
<div className="uppy-Dashboard-AddFiles">
{this.renderHiddenInput(false, (ref: $TSFixMe) => {
{this.renderHiddenInput(false, (ref) => {
this.fileInput = ref
})}
{this.renderHiddenInput(true, (ref: $TSFixMe) => {
{this.renderHiddenInput(true, (ref) => {
this.folderInput = ref
})}
{showNativePhotoCameraButton &&
this.renderHiddenCameraInput(
'photo',
nativeCameraFacingMode,
(ref: $TSFixMe) => {
(ref) => {
this.mobilePhotoFileInput = ref
},
)}
{showNativeVideoCameraButton &&
this.renderHiddenCameraInput(
'video',
nativeCameraFacingMode,
(ref: $TSFixMe) => {
(ref) => {
this.mobileVideoFileInput = ref
},
)}
@@ -438,8 +465,7 @@ class AddFiles extends Component {
{this.props.note && (
<div className="uppy-Dashboard-note">{this.props.note}</div>
)}
{this.props.proudlyDisplayPoweredByUppy &&
this.renderPoweredByUppy(this.props)}
{this.props.proudlyDisplayPoweredByUppy && this.renderPoweredByUppy()}
</div>
</div>
)
185 changes: 148 additions & 37 deletions packages/@uppy/dashboard/src/components/Dashboard.tsx
Original file line number Diff line number Diff line change
@@ -2,6 +2,11 @@
import { h } from 'preact'
import classNames from 'classnames'
import isDragDropSupported from '@uppy/utils/lib/isDragDropSupported'
import type { Body, Meta, UppyFile } from '@uppy/utils/lib/UppyFile'
import type { State, UIPlugin, UIPluginOptions, Uppy } from '@uppy/core'
import type { I18n } from '@uppy/utils/lib/Translator'
import type Translator from '@uppy/utils/lib/Translator'
import type { TargetedEvent } from 'preact/compat'
import FileList from './FileList.tsx'
import AddFiles from './AddFiles.tsx'
import AddFilesPanel from './AddFilesPanel.tsx'
@@ -10,6 +15,7 @@ import EditorPanel from './EditorPanel.tsx'
import PanelTopBar from './PickerPanelTopBar.tsx'
import FileCard from './FileCard/index.tsx'
import Slide from './Slide.tsx'
import type { DashboardState, TargetWithRender } from '../Dashboard'

// http://dev.edenspiekermann.com/2016/02/11/introducing-accessible-modal-dialog
// https://github.com/ghosh/micromodal
@@ -23,9 +29,95 @@ const HEIGHT_MD = 330
// const HEIGHT_LG = 400
// const HEIGHT_XL = 460

type $TSFixMe = any
type DashboardUIProps<M extends Meta, B extends Body> = {
state: State<M, B>
isHidden: boolean
files: State<M, B>['files']
newFiles: UppyFile<M, B>[]
uploadStartedFiles: UppyFile<M, B>[]
completeFiles: UppyFile<M, B>[]
erroredFiles: UppyFile<M, B>[]
inProgressFiles: UppyFile<M, B>[]
inProgressNotPausedFiles: UppyFile<M, B>[]
processingFiles: UppyFile<M, B>[]
isUploadStarted: boolean
isAllComplete: boolean
isAllPaused: boolean
totalFileCount: number
totalProgress: number
allowNewUpload: boolean
acquirers: TargetWithRender[]
theme: string
disabled: boolean
disableLocalFiles: boolean
direction: UIPluginOptions['direction']
activePickerPanel: DashboardState<M, B>['activePickerPanel']
showFileEditor: boolean
saveFileEditor: () => void
closeFileEditor: () => void
disableInteractiveElements: (disable: boolean) => void
animateOpenClose: boolean
isClosing: boolean
progressindicators: TargetWithRender[]
editors: TargetWithRender[]
autoProceed: boolean
id: string
closeModal: () => void
handleClickOutside: () => void
handleInputChange: (event: TargetedEvent<HTMLInputElement, Event>) => void
handlePaste: (event: ClipboardEvent) => void
inline: boolean
showPanel: (id: string) => void
hideAllPanels: () => void
i18n: I18n
i18nArray: Translator['translateArray']
uppy: Uppy<M, B>
note: string | null
recoveredState: State<M, B>['recoveredState']
metaFields: DashboardState<M, B>['metaFields']
resumableUploads: boolean
individualCancellation: boolean
isMobileDevice?: boolean
fileCardFor: string | null
toggleFileCard: (show: boolean, fileID: string) => void
toggleAddFilesPanel: (show: boolean) => void
showAddFilesPanel: boolean
saveFileCard: (meta: M, fileID: string) => void
openFileEditor: (file: UppyFile<M, B>) => void
canEditFile: (file: UppyFile<M, B>) => boolean
width: string | number
height: string | number
showLinkToFileUploadResult: boolean
fileManagerSelectionType: string
proudlyDisplayPoweredByUppy: boolean
hideCancelButton: boolean
hideRetryButton: boolean
hidePauseResumeButton: boolean
showRemoveButtonAfterComplete: boolean
containerWidth: number
containerHeight: number
areInsidesReadyToBeVisible: boolean
parentElement: HTMLElement | null
allowedFileTypes: string[] | null
maxNumberOfFiles: number | null
requiredMetaFields: any
showSelectedFiles: boolean
showNativePhotoCameraButton: boolean
showNativeVideoCameraButton: boolean
nativeCameraFacingMode: 'user' | 'environment' | ''
singleFileFullScreen: boolean
handleCancelRestore: () => void
handleRequestThumbnail: (file: UppyFile<M, B>) => void
handleCancelThumbnail: (file: UppyFile<M, B>) => void
isDraggingOver: boolean
handleDragOver: (event: DragEvent) => void
handleDragLeave: (event: DragEvent) => void
handleDrop: (event: DragEvent) => void
}

export default function Dashboard(props: $TSFixMe) {
export default function Dashboard<M extends Meta, B extends Body>(
props: DashboardUIProps<M, B>,
) {
const isNoFiles = props.totalFileCount === 0
const isSingleFile = props.totalFileCount === 1
const isSizeMD = props.containerWidth > WIDTH_MD
@@ -70,10 +162,10 @@ export default function Dashboard(props: $TSFixMe) {
props.files ?
Object.keys(props.files).filter((fileID) => props.files[fileID].isGhost)
.length
: null
: 0

const renderRestoredText = () => {
if (numberOfGhosts! > 0) {
if (numberOfGhosts > 0) {
return props.i18n('recoveredXFiles', {
smart_count: numberOfGhosts,
})
@@ -166,37 +258,51 @@ export default function Dashboard(props: $TSFixMe) {
</div>
)}

{
showFileList ?
<FileList
id={props.id}
i18n={props.i18n}
uppy={props.uppy}
files={props.files}
resumableUploads={props.resumableUploads}
hideRetryButton={props.hideRetryButton}
hidePauseResumeButton={props.hidePauseResumeButton}
hideCancelButton={props.hideCancelButton}
showLinkToFileUploadResult={props.showLinkToFileUploadResult}
showRemoveButtonAfterComplete={
props.showRemoveButtonAfterComplete
}
metaFields={props.metaFields}
toggleFileCard={props.toggleFileCard}
handleRequestThumbnail={props.handleRequestThumbnail}
handleCancelThumbnail={props.handleCancelThumbnail}
recoveredState={props.recoveredState}
individualCancellation={props.individualCancellation}
openFileEditor={props.openFileEditor}
canEditFile={props.canEditFile}
toggleAddFilesPanel={props.toggleAddFilesPanel}
isSingleFile={isSingleFile}
itemsPerRow={itemsPerRow}
containerWidth={props.containerWidth}
containerHeight={props.containerHeight}
/>
// eslint-disable-next-line react/jsx-props-no-spreading
: <AddFiles {...props} isSizeMD={isSizeMD} />
{showFileList ?
<FileList
id={props.id}
i18n={props.i18n}
uppy={props.uppy}
files={props.files}
resumableUploads={props.resumableUploads}
hideRetryButton={props.hideRetryButton}
hidePauseResumeButton={props.hidePauseResumeButton}
hideCancelButton={props.hideCancelButton}
showLinkToFileUploadResult={props.showLinkToFileUploadResult}
showRemoveButtonAfterComplete={
props.showRemoveButtonAfterComplete
}
metaFields={props.metaFields}
toggleFileCard={props.toggleFileCard}
handleRequestThumbnail={props.handleRequestThumbnail}
handleCancelThumbnail={props.handleCancelThumbnail}
recoveredState={props.recoveredState}
individualCancellation={props.individualCancellation}
openFileEditor={props.openFileEditor}
canEditFile={props.canEditFile}
toggleAddFilesPanel={props.toggleAddFilesPanel}
isSingleFile={isSingleFile}
itemsPerRow={itemsPerRow}
containerWidth={props.containerWidth}
containerHeight={props.containerHeight}
/>
: <AddFiles
i18n={props.i18n}
i18nArray={props.i18nArray}
acquirers={props.acquirers}
handleInputChange={props.handleInputChange}
maxNumberOfFiles={props.maxNumberOfFiles}
allowedFileTypes={props.allowedFileTypes}
showNativePhotoCameraButton={props.showNativePhotoCameraButton}
showNativeVideoCameraButton={props.showNativeVideoCameraButton}
nativeCameraFacingMode={props.nativeCameraFacingMode}
showPanel={props.showPanel}
activePickerPanel={props.activePickerPanel}
disableLocalFiles={props.disableLocalFiles}
fileManagerSelectionType={props.fileManagerSelectionType}
note={props.note}
proudlyDisplayPoweredByUppy={props.proudlyDisplayPoweredByUppy}
/>
}

<Slide>
@@ -228,8 +334,13 @@ export default function Dashboard(props: $TSFixMe) {
</Slide>

<div className="uppy-Dashboard-progressindicators">
{props.progressindicators.map((target: $TSFixMe) => {
return props.uppy.getPlugin(target.id).render(props.state)
{props.progressindicators.map((target: TargetWithRender) => {
// TODO
// Here we're telling typescript all `this.type = 'progressindicator'` plugins inherit from `UIPlugin`
// This is factually true in Uppy right now, but maybe it doesn't have to be
return (
props.uppy.getPlugin(target.id) as UIPlugin<any, any, any>
).render(props.state)
})}
</div>
</div>
4 changes: 3 additions & 1 deletion packages/@uppy/utils/src/Translator.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import type { h } from 'preact'

// We're using a generic because languages have different plural rules.
export interface Locale<T extends number = number> {
strings: Record<string, string | Record<T, string>>
@@ -14,7 +16,7 @@ export type I18n = Translator['translate']
type Options = {
smart_count?: number
} & {
[key: string]: string | number
[key: string]: string | number | h.JSX.Element
}

function insertReplacement(

0 comments on commit 922e7fb

Please sign in to comment.