123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544 |
- import type { Body, Meta } from '@uppy/utils/lib/UppyFile'
- import type { State, Uppy } from '@uppy/core/lib/Uppy'
- import type { FileProcessingInfo } from '@uppy/utils/lib/FileProgress'
- import type { I18n } from '@uppy/utils/lib/Translator'
- import { h } from 'preact'
- import classNames from 'classnames'
- import prettierBytes from '@transloadit/prettier-bytes'
- import prettyETA from '@uppy/utils/lib/prettyETA'
- import statusBarStates from './StatusBarStates.ts'
- 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
- totalETA: number
- }
- function ProgressDetails(props: ProgressDetailsProps) {
- const { numUploads, complete, totalUploadedSize, totalSize, totalETA, i18n } =
- props
- const ifShowFilesUploadedOfTotal = numUploads > 1
- 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()}
- {i18n('dataUploadedOfTotal', {
- complete: prettierBytes(totalUploadedSize),
- total: prettierBytes(totalSize),
- })}
- {renderDot()}
- {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
- totalETA: number
- 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" aria-label={title} title={title}>
- {!isAllPaused ?
- <LoadingSpinner />
- : null}
- <div className="uppy-StatusBar-status">
- <div className="uppy-StatusBar-statusPrimary">
- {supportsUploadProgress ? `${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"
- 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}`
- // eslint-disable-next-line no-alert
- 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,
- }
|