mirror of
https://github.com/transloadit/uppy.git
synced 2026-01-23 02:25:07 +00:00
547 lines
14 KiB
TypeScript
547 lines
14 KiB
TypeScript
import prettierBytes from '@transloadit/prettier-bytes'
|
|
import type { Body, Meta, State, Uppy } from '@uppy/core'
|
|
import type { FileProcessingInfo } from '@uppy/utils/lib/FileProgress'
|
|
import prettyETA from '@uppy/utils/lib/prettyETA'
|
|
import type { I18n } from '@uppy/utils/lib/Translator'
|
|
import classNames from 'classnames'
|
|
|
|
import statusBarStates from './StatusBarStates.js'
|
|
|
|
const DOT = `\u00B7`
|
|
const renderDot = (): string => ` ${DOT} `
|
|
|
|
interface UploadBtnProps<M extends Meta, B extends Body> {
|
|
newFiles: number
|
|
isUploadStarted: boolean
|
|
recoveredState: State<M, B>['recoveredState']
|
|
i18n: I18n
|
|
uploadState: string
|
|
isSomeGhost: boolean
|
|
startUpload: () => void
|
|
}
|
|
|
|
function UploadBtn<M extends Meta, B extends Body>(
|
|
props: UploadBtnProps<M, B>,
|
|
) {
|
|
const {
|
|
newFiles,
|
|
isUploadStarted,
|
|
recoveredState,
|
|
i18n,
|
|
uploadState,
|
|
isSomeGhost,
|
|
startUpload,
|
|
} = props
|
|
|
|
const uploadBtnClassNames = classNames(
|
|
'uppy-u-reset',
|
|
'uppy-c-btn',
|
|
'uppy-StatusBar-actionBtn',
|
|
'uppy-StatusBar-actionBtn--upload',
|
|
{
|
|
'uppy-c-btn-primary': uploadState === statusBarStates.STATE_WAITING,
|
|
},
|
|
{ 'uppy-StatusBar-actionBtn--disabled': isSomeGhost },
|
|
)
|
|
|
|
const uploadBtnText =
|
|
newFiles && isUploadStarted && !recoveredState
|
|
? i18n('uploadXNewFiles', { smart_count: newFiles })
|
|
: i18n('uploadXFiles', { smart_count: newFiles })
|
|
|
|
return (
|
|
<button
|
|
type="button"
|
|
className={uploadBtnClassNames}
|
|
aria-label={i18n('uploadXFiles', { smart_count: newFiles })}
|
|
onClick={startUpload}
|
|
disabled={isSomeGhost}
|
|
data-uppy-super-focusable
|
|
>
|
|
{uploadBtnText}
|
|
</button>
|
|
)
|
|
}
|
|
|
|
interface RetryBtnProps<M extends Meta, B extends Body> {
|
|
i18n: I18n
|
|
uppy: Uppy<M, B>
|
|
}
|
|
|
|
function RetryBtn<M extends Meta, B extends Body>(props: RetryBtnProps<M, B>) {
|
|
const { i18n, uppy } = props
|
|
|
|
return (
|
|
<button
|
|
type="button"
|
|
className="uppy-u-reset uppy-c-btn uppy-StatusBar-actionBtn uppy-StatusBar-actionBtn--retry"
|
|
aria-label={i18n('retryUpload')}
|
|
onClick={() =>
|
|
uppy.retryAll().catch(() => {
|
|
/* Error reported and handled via an event */
|
|
})
|
|
}
|
|
data-uppy-super-focusable
|
|
data-cy="retry"
|
|
>
|
|
<svg
|
|
aria-hidden="true"
|
|
focusable="false"
|
|
className="uppy-c-icon"
|
|
width="8"
|
|
height="10"
|
|
viewBox="0 0 8 10"
|
|
>
|
|
<path d="M4 2.408a2.75 2.75 0 1 0 2.75 2.75.626.626 0 0 1 1.25.018v.023a4 4 0 1 1-4-4.041V.25a.25.25 0 0 1 .389-.208l2.299 1.533a.25.25 0 0 1 0 .416l-2.3 1.533A.25.25 0 0 1 4 3.316v-.908z" />
|
|
</svg>
|
|
{i18n('retry')}
|
|
</button>
|
|
)
|
|
}
|
|
|
|
interface CancelBtnProps<M extends Meta, B extends Body> {
|
|
i18n: I18n
|
|
uppy: Uppy<M, B>
|
|
}
|
|
|
|
function CancelBtn<M extends Meta, B extends Body>(
|
|
props: CancelBtnProps<M, B>,
|
|
) {
|
|
const { i18n, uppy } = props
|
|
|
|
return (
|
|
<button
|
|
type="button"
|
|
className="uppy-u-reset uppy-StatusBar-actionCircleBtn"
|
|
title={i18n('cancel')}
|
|
aria-label={i18n('cancel')}
|
|
onClick={(): void => uppy.cancelAll()}
|
|
data-cy="cancel"
|
|
data-uppy-super-focusable
|
|
>
|
|
<svg
|
|
aria-hidden="true"
|
|
focusable="false"
|
|
className="uppy-c-icon"
|
|
width="16"
|
|
height="16"
|
|
viewBox="0 0 16 16"
|
|
>
|
|
<g fill="none" fillRule="evenodd">
|
|
<circle fill="#888" cx="8" cy="8" r="8" />
|
|
<path
|
|
fill="#FFF"
|
|
d="M9.283 8l2.567 2.567-1.283 1.283L8 9.283 5.433 11.85 4.15 10.567 6.717 8 4.15 5.433 5.433 4.15 8 6.717l2.567-2.567 1.283 1.283z"
|
|
/>
|
|
</g>
|
|
</svg>
|
|
</button>
|
|
)
|
|
}
|
|
|
|
interface PauseResumeButtonProps<M extends Meta, B extends Body> {
|
|
i18n: I18n
|
|
uppy: Uppy<M, B>
|
|
isAllPaused: boolean
|
|
isAllComplete: boolean
|
|
resumableUploads: boolean
|
|
}
|
|
|
|
function PauseResumeButton<M extends Meta, B extends Body>(
|
|
props: PauseResumeButtonProps<M, B>,
|
|
) {
|
|
const { isAllPaused, i18n, isAllComplete, resumableUploads, uppy } = props
|
|
const title = isAllPaused ? i18n('resume') : i18n('pause')
|
|
|
|
function togglePauseResume(): void {
|
|
if (isAllComplete) return
|
|
|
|
if (!resumableUploads) {
|
|
uppy.cancelAll()
|
|
return
|
|
}
|
|
|
|
if (isAllPaused) {
|
|
uppy.resumeAll()
|
|
return
|
|
}
|
|
|
|
uppy.pauseAll()
|
|
}
|
|
|
|
return (
|
|
<button
|
|
title={title}
|
|
aria-label={title}
|
|
className="uppy-u-reset uppy-StatusBar-actionCircleBtn"
|
|
type="button"
|
|
onClick={togglePauseResume}
|
|
data-cy="togglePauseResume"
|
|
data-uppy-super-focusable
|
|
>
|
|
<svg
|
|
aria-hidden="true"
|
|
focusable="false"
|
|
className="uppy-c-icon"
|
|
width="16"
|
|
height="16"
|
|
viewBox="0 0 16 16"
|
|
>
|
|
<g fill="none" fillRule="evenodd">
|
|
<circle fill="#888" cx="8" cy="8" r="8" />
|
|
<path
|
|
fill="#FFF"
|
|
d={
|
|
isAllPaused
|
|
? 'M6 4.25L11.5 8 6 11.75z'
|
|
: 'M5 4.5h2v7H5v-7zm4 0h2v7H9v-7z'
|
|
}
|
|
/>
|
|
</g>
|
|
</svg>
|
|
</button>
|
|
)
|
|
}
|
|
|
|
interface DoneBtnProps {
|
|
i18n: I18n
|
|
doneButtonHandler: (() => void) | undefined
|
|
}
|
|
|
|
function DoneBtn(props: DoneBtnProps) {
|
|
const { i18n, doneButtonHandler } = props
|
|
|
|
return (
|
|
<button
|
|
type="button"
|
|
className="uppy-u-reset uppy-c-btn uppy-StatusBar-actionBtn uppy-StatusBar-actionBtn--done"
|
|
onClick={doneButtonHandler}
|
|
data-uppy-super-focusable
|
|
>
|
|
{i18n('done')}
|
|
</button>
|
|
)
|
|
}
|
|
|
|
function LoadingSpinner() {
|
|
return (
|
|
<svg
|
|
className="uppy-StatusBar-spinner"
|
|
aria-hidden="true"
|
|
focusable="false"
|
|
width="14"
|
|
height="14"
|
|
>
|
|
<path
|
|
d="M13.983 6.547c-.12-2.509-1.64-4.893-3.939-5.936-2.48-1.127-5.488-.656-7.556 1.094C.524 3.367-.398 6.048.162 8.562c.556 2.495 2.46 4.52 4.94 5.183 2.932.784 5.61-.602 7.256-3.015-1.493 1.993-3.745 3.309-6.298 2.868-2.514-.434-4.578-2.349-5.153-4.84a6.226 6.226 0 0 1 2.98-6.778C6.34.586 9.74 1.1 11.373 3.493c.407.596.693 1.282.842 1.988.127.598.073 1.197.161 1.794.078.525.543 1.257 1.15.864.525-.341.49-1.05.456-1.592-.007-.15.02.3 0 0"
|
|
fillRule="evenodd"
|
|
/>
|
|
</svg>
|
|
)
|
|
}
|
|
|
|
interface ProgressBarProcessingProps {
|
|
progress: FileProcessingInfo
|
|
}
|
|
|
|
function ProgressBarProcessing(props: ProgressBarProcessingProps) {
|
|
const { progress } = props
|
|
const { value, mode, message } = progress
|
|
const dot = `\u00B7`
|
|
|
|
return (
|
|
<div className="uppy-StatusBar-content">
|
|
<LoadingSpinner />
|
|
{mode === 'determinate' ? `${Math.round(value * 100)}% ${dot} ` : ''}
|
|
{message}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
interface ProgressDetailsProps {
|
|
i18n: I18n
|
|
numUploads: number
|
|
complete: number
|
|
totalUploadedSize: number
|
|
totalSize: number | null
|
|
totalETA: number | null
|
|
}
|
|
|
|
function ProgressDetails(props: ProgressDetailsProps) {
|
|
const { numUploads, complete, totalUploadedSize, totalSize, totalETA, i18n } =
|
|
props
|
|
|
|
const ifShowFilesUploadedOfTotal = numUploads > 1
|
|
|
|
const totalUploadedSizeStr = prettierBytes(totalUploadedSize)
|
|
|
|
return (
|
|
<div className="uppy-StatusBar-statusSecondary">
|
|
{ifShowFilesUploadedOfTotal &&
|
|
i18n('filesUploadedOfTotal', {
|
|
complete,
|
|
smart_count: numUploads,
|
|
})}
|
|
<span className="uppy-StatusBar-additionalInfo">
|
|
{/* When should we render this dot?
|
|
1. .-additionalInfo is shown (happens only on desktops)
|
|
2. AND 'filesUploadedOfTotal' was shown
|
|
*/}
|
|
{ifShowFilesUploadedOfTotal && renderDot()}
|
|
|
|
{totalSize != null
|
|
? i18n('dataUploadedOfTotal', {
|
|
complete: totalUploadedSizeStr,
|
|
total: prettierBytes(totalSize),
|
|
})
|
|
: i18n('dataUploadedOfUnknown', { complete: totalUploadedSizeStr })}
|
|
|
|
{renderDot()}
|
|
|
|
{totalETA != null &&
|
|
i18n('xTimeLeft', {
|
|
time: prettyETA(totalETA),
|
|
})}
|
|
</span>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
interface FileUploadCountProps {
|
|
i18n: I18n
|
|
complete: number
|
|
numUploads: number
|
|
}
|
|
|
|
function FileUploadCount(props: FileUploadCountProps) {
|
|
const { i18n, complete, numUploads } = props
|
|
|
|
return (
|
|
<div className="uppy-StatusBar-statusSecondary">
|
|
{i18n('filesUploadedOfTotal', { complete, smart_count: numUploads })}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
interface UploadNewlyAddedFilesProps {
|
|
i18n: I18n
|
|
newFiles: number
|
|
startUpload: () => void
|
|
}
|
|
|
|
function UploadNewlyAddedFiles(props: UploadNewlyAddedFilesProps) {
|
|
const { i18n, newFiles, startUpload } = props
|
|
const uploadBtnClassNames = classNames(
|
|
'uppy-u-reset',
|
|
'uppy-c-btn',
|
|
'uppy-StatusBar-actionBtn',
|
|
'uppy-StatusBar-actionBtn--uploadNewlyAdded',
|
|
)
|
|
|
|
return (
|
|
<div className="uppy-StatusBar-statusSecondary">
|
|
<div className="uppy-StatusBar-statusSecondaryHint">
|
|
{i18n('xMoreFilesAdded', { smart_count: newFiles })}
|
|
</div>
|
|
<button
|
|
type="button"
|
|
className={uploadBtnClassNames}
|
|
aria-label={i18n('uploadXFiles', { smart_count: newFiles })}
|
|
onClick={startUpload}
|
|
>
|
|
{i18n('upload')}
|
|
</button>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
interface ProgressBarUploadingProps {
|
|
i18n: I18n
|
|
supportsUploadProgress: boolean
|
|
totalProgress: number
|
|
showProgressDetails: boolean | undefined
|
|
isUploadStarted: boolean
|
|
isAllComplete: boolean
|
|
isAllPaused: boolean
|
|
newFiles: number
|
|
numUploads: number
|
|
complete: number
|
|
totalUploadedSize: number
|
|
totalSize: number | null
|
|
totalETA: number | null
|
|
startUpload: () => void
|
|
}
|
|
|
|
function ProgressBarUploading(props: ProgressBarUploadingProps) {
|
|
const {
|
|
i18n,
|
|
supportsUploadProgress,
|
|
totalProgress,
|
|
showProgressDetails,
|
|
isUploadStarted,
|
|
isAllComplete,
|
|
isAllPaused,
|
|
newFiles,
|
|
numUploads,
|
|
complete,
|
|
totalUploadedSize,
|
|
totalSize,
|
|
totalETA,
|
|
startUpload,
|
|
} = props
|
|
|
|
const showUploadNewlyAddedFiles = newFiles && isUploadStarted
|
|
|
|
if (!isUploadStarted || isAllComplete) {
|
|
return null
|
|
}
|
|
|
|
const title = isAllPaused ? i18n('paused') : i18n('uploading')
|
|
|
|
function renderProgressDetails() {
|
|
if (!isAllPaused && !showUploadNewlyAddedFiles && showProgressDetails) {
|
|
if (supportsUploadProgress) {
|
|
return (
|
|
<ProgressDetails
|
|
numUploads={numUploads}
|
|
complete={complete}
|
|
totalUploadedSize={totalUploadedSize}
|
|
totalSize={totalSize}
|
|
totalETA={totalETA}
|
|
i18n={i18n}
|
|
/>
|
|
)
|
|
}
|
|
return (
|
|
<FileUploadCount
|
|
i18n={i18n}
|
|
complete={complete}
|
|
numUploads={numUploads}
|
|
/>
|
|
)
|
|
}
|
|
return null
|
|
}
|
|
|
|
return (
|
|
<div className="uppy-StatusBar-content" title={title}>
|
|
{!isAllPaused ? <LoadingSpinner /> : null}
|
|
<div className="uppy-StatusBar-status">
|
|
<div className="uppy-StatusBar-statusPrimary">
|
|
{supportsUploadProgress && totalProgress !== 0
|
|
? `${title}: ${totalProgress}%`
|
|
: title}
|
|
</div>
|
|
|
|
{renderProgressDetails()}
|
|
|
|
{showUploadNewlyAddedFiles ? (
|
|
<UploadNewlyAddedFiles
|
|
i18n={i18n}
|
|
newFiles={newFiles}
|
|
startUpload={startUpload}
|
|
/>
|
|
) : null}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
interface ProgressBarCompleteProps {
|
|
i18n: I18n
|
|
}
|
|
|
|
function ProgressBarComplete(props: ProgressBarCompleteProps) {
|
|
const { i18n } = props
|
|
|
|
return (
|
|
<div
|
|
className="uppy-StatusBar-content"
|
|
// biome-ignore lint/a11y/useSemanticElements: ...
|
|
role="status"
|
|
title={i18n('complete')}
|
|
>
|
|
<div className="uppy-StatusBar-status">
|
|
<div className="uppy-StatusBar-statusPrimary">
|
|
<svg
|
|
aria-hidden="true"
|
|
focusable="false"
|
|
className="uppy-StatusBar-statusIndicator uppy-c-icon"
|
|
width="15"
|
|
height="11"
|
|
viewBox="0 0 15 11"
|
|
>
|
|
<path d="M.414 5.843L1.627 4.63l3.472 3.472L13.202 0l1.212 1.213L5.1 10.528z" />
|
|
</svg>
|
|
{i18n('complete')}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
interface ProgressBarErrorProps {
|
|
i18n: I18n
|
|
error: any
|
|
complete: number
|
|
numUploads: number
|
|
}
|
|
|
|
function ProgressBarError(props: ProgressBarErrorProps) {
|
|
const { error, i18n, complete, numUploads } = props
|
|
|
|
function displayErrorAlert(): void {
|
|
const errorMessage = `${i18n('uploadFailed')} \n\n ${error}`
|
|
alert(errorMessage) // TODO: move to custom alert implementation
|
|
}
|
|
|
|
return (
|
|
<div className="uppy-StatusBar-content" title={i18n('uploadFailed')}>
|
|
<svg
|
|
aria-hidden="true"
|
|
focusable="false"
|
|
className="uppy-StatusBar-statusIndicator uppy-c-icon"
|
|
width="11"
|
|
height="11"
|
|
viewBox="0 0 11 11"
|
|
>
|
|
<path d="M4.278 5.5L0 1.222 1.222 0 5.5 4.278 9.778 0 11 1.222 6.722 5.5 11 9.778 9.778 11 5.5 6.722 1.222 11 0 9.778z" />
|
|
</svg>
|
|
<div className="uppy-StatusBar-status">
|
|
<div className="uppy-StatusBar-statusPrimary">
|
|
{i18n('uploadFailed')}
|
|
|
|
<button
|
|
className="uppy-u-reset uppy-StatusBar-details"
|
|
aria-label={i18n('showErrorDetails')}
|
|
data-microtip-position="top-right"
|
|
data-microtip-size="medium"
|
|
onClick={displayErrorAlert}
|
|
type="button"
|
|
>
|
|
?
|
|
</button>
|
|
</div>
|
|
|
|
<FileUploadCount
|
|
i18n={i18n}
|
|
complete={complete}
|
|
numUploads={numUploads}
|
|
/>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
export {
|
|
UploadBtn,
|
|
RetryBtn,
|
|
CancelBtn,
|
|
PauseResumeButton,
|
|
DoneBtn,
|
|
LoadingSpinner,
|
|
ProgressDetails,
|
|
ProgressBarProcessing,
|
|
ProgressBarError,
|
|
ProgressBarUploading,
|
|
ProgressBarComplete,
|
|
}
|