Explorar el Código

Show all details on mobile when `showProgressDetails` is `true` (#3174)

Merlijn Vos hace 3 años
padre
commit
a298e59f17

+ 1 - 1
.eslintrc.js

@@ -85,7 +85,7 @@ module.exports = {
     'no-param-reassign': ['warn'],
     'no-redeclare': ['warn'],
     'no-shadow': ['warn'],
-    'no-use-before-define': ['warn'],
+    'no-use-before-define': ['warn', { 'functions': false }],
     'radix': ['warn'],
     'react/button-has-type': 'error',
     'react/destructuring-assignment': ['warn'],

+ 0 - 1
BACKLOG.md

@@ -50,7 +50,6 @@ PRs are welcome! Please do open an issue to discuss first if it's a big feature,
 - [ ] Display data like image resolution on file cards. should be done by thumbnail generator maybe #783
 - [ ] Possibility to edit/delete more than one file at once. example: add copyrigh info to 1000 files #118, #97
 - [ ] Possibility to work on already uploaded / in progress files. We'll just provide the `fileId` to the `file-edit-complete` event so that folks can more easily roll out custom code for this themselves #112, #113, #2063
-- [ ] Show upload speed too if `showProgressDetails: true`. Maybe have separate options for which things are displayed, or at least have css-classes that can be hidden with `display: none` #766
 - [ ] Focus jumps weirdly if you remove a file https://github.com/transloadit/uppy/pull/2161#issuecomment-613565486
 - [ ] A mini UI that features drop & progress (may involve a `mini: true` options for dashboard, may involve drop+progress or new plugin) (@arturi)
 - [ ] Add a Load More button so you don't have to TAB endlessly to get to the upload button (https://github.com/transloadit/uppy/issues/1419)

+ 0 - 5
packages/@uppy/dashboard/src/style.scss

@@ -705,11 +705,6 @@
   height: 100%;
 }
 
-// Do not show progress details in the StatusBar if we do not have space.
-.uppy-Dashboard:not(.uppy-size--md) .uppy-StatusBar-additionalInfo {
-  display: none;
-}
-
 .uppy-Dashboard-filesContainer {
   @include clearfix;
 

+ 446 - 0
packages/@uppy/status-bar/src/Components.js

@@ -0,0 +1,446 @@
+const classNames = require('classnames')
+const throttle = require('lodash.throttle')
+const prettierBytes = require('@transloadit/prettier-bytes')
+const prettyETA = require('@uppy/utils/lib/prettyETA')
+const { h } = require('preact')
+
+const statusBarStates = require('./StatusBarStates')
+
+const DOT = `\u00B7`
+const renderDot = () => ` ${DOT} `
+
+function UploadBtn (props) {
+  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>
+  )
+}
+
+function RetryBtn (props) {
+  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()}
+      data-uppy-super-focusable
+    >
+      <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>
+  )
+}
+
+function CancelBtn (props) {
+  const { i18n, uppy } = props
+
+  return (
+    <button
+      type="button"
+      className="uppy-u-reset uppy-StatusBar-actionCircleBtn"
+      title={i18n('cancel')}
+      aria-label={i18n('cancel')}
+      onClick={() => uppy.cancelAll()}
+      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>
+  )
+}
+
+function PauseResumeButton (props) {
+  const { isAllPaused, i18n, isAllComplete, resumableUploads, uppy } = props
+  const title = isAllPaused ? i18n('resume') : i18n('pause')
+
+  function togglePauseResume () {
+    if (isAllComplete) return null
+
+    if (!resumableUploads) {
+      return uppy.cancelAll()
+    }
+
+    if (isAllPaused) {
+      return uppy.resumeAll()
+    }
+
+    return uppy.pauseAll()
+  }
+
+  return (
+    <button
+      title={title}
+      aria-label={title}
+      className="uppy-u-reset uppy-StatusBar-actionCircleBtn"
+      type="button"
+      onClick={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>
+  )
+}
+
+function DoneBtn (props) {
+  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>
+  )
+}
+
+function ProgressBarProcessing (props) {
+  const { value, mode, message } = props
+  const roundedValue = Math.round(value * 100)
+  const dot = `\u00B7`
+
+  return (
+    <div className="uppy-StatusBar-content">
+      <LoadingSpinner />
+      {mode === 'determinate' ? `${roundedValue}% ${dot} ` : ''}
+      {message}
+    </div>
+  )
+}
+
+function ProgressDetails (props) {
+  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>
+  )
+}
+
+function UnknownProgressDetails (props) {
+  const { i18n, complete, numUploads } = props
+
+  return (
+    <div className="uppy-StatusBar-statusSecondary">
+      {i18n('filesUploadedOfTotal', { complete, smart_count: numUploads })}
+    </div>
+  )
+}
+
+function UploadNewlyAddedFiles (props) {
+  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>
+  )
+}
+
+const ThrottledProgressDetails = throttle(ProgressDetails, 500, {
+  leading: true,
+  trailing: true,
+})
+
+function ProgressBarUploading (props) {
+  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 (
+          <ThrottledProgressDetails
+            numUploads={numUploads}
+            complete={complete}
+            totalUploadedSize={totalUploadedSize}
+            totalSize={totalSize}
+            totalETA={totalETA}
+            i18n={i18n}
+          />
+        )
+      }
+      return (
+        <UnknownProgressDetails
+          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>
+  )
+}
+
+function ProgressBarComplete (props) {
+  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>
+  )
+}
+
+function ProgressBarError (props) {
+  const { error, i18n } = props
+
+  function displayErrorAlert () {
+    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"
+      role="alert"
+      title={i18n('uploadFailed')}
+    >
+      <div className="uppy-StatusBar-status">
+        <div className="uppy-StatusBar-statusPrimary">
+          <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>
+          {i18n('uploadFailed')}
+        </div>
+      </div>
+      <button
+        className="uppy-StatusBar-details"
+        aria-label={error}
+        data-microtip-position="top-right"
+        data-microtip-size="medium"
+        onClick={displayErrorAlert}
+        type="button"
+      >
+        ?
+      </button>
+    </div>
+  )
+}
+
+module.exports = {
+  UploadBtn,
+  RetryBtn,
+  CancelBtn,
+  PauseResumeButton,
+  DoneBtn,
+  LoadingSpinner,
+  ProgressDetails,
+  ProgressBarProcessing,
+  ProgressBarError,
+  ProgressBarUploading,
+  ProgressBarComplete,
+}

+ 187 - 374
packages/@uppy/status-bar/src/StatusBar.js

@@ -1,57 +1,32 @@
-const throttle = require('lodash.throttle')
-const classNames = require('classnames')
-const prettierBytes = require('@transloadit/prettier-bytes')
-const prettyETA = require('@uppy/utils/lib/prettyETA')
 const { h } = require('preact')
+const classNames = require('classnames')
 const statusBarStates = require('./StatusBarStates')
-
-function calculateProcessingProgress (files) {
-  // Collect pre or postprocessing progress states.
-  const progresses = []
-  Object.keys(files).forEach((fileID) => {
-    const { progress } = files[fileID]
-    if (progress.preprocess) {
-      progresses.push(progress.preprocess)
-    }
-    if (progress.postprocess) {
-      progresses.push(progress.postprocess)
-    }
-  })
-
-  // In the future we should probably do this differently. For now we'll take the
-  // mode and message from the first file…
-  const { mode, message } = progresses[0]
-  const value = progresses.filter(isDeterminate).reduce((total, progress, index, all) => {
-    return total + progress.value / all.length
-  }, 0)
-  function isDeterminate (progress) {
-    return progress.mode === 'determinate'
-  }
-
-  return {
-    mode,
-    message,
-    value,
-  }
-}
-
-function togglePauseResume (props) {
-  if (props.isAllComplete) return
-
-  if (!props.resumableUploads) {
-    return props.uppy.cancelAll()
-  }
-
-  if (props.isAllPaused) {
-    return props.uppy.resumeAll()
-  }
-
-  return props.uppy.pauseAll()
-}
-
-module.exports = (props) => {
-  props = props || {}
-
+const calculateProcessingProgress = require('./calculateProcessingProgress')
+
+const {
+  UploadBtn,
+  RetryBtn,
+  CancelBtn,
+  PauseResumeButton,
+  DoneBtn,
+  ProgressBarProcessing,
+  ProgressBarError,
+  ProgressBarUploading,
+  ProgressBarComplete,
+} = require('./Components')
+
+const {
+  STATE_ERROR,
+  STATE_WAITING,
+  STATE_PREPROCESSING,
+  STATE_UPLOADING,
+  STATE_POSTPROCESSING,
+  STATE_COMPLETE,
+} = statusBarStates
+
+module.exports = StatusBar
+
+function StatusBar (props) {
   const {
     newFiles,
     allowNewUpload,
@@ -64,68 +39,122 @@ module.exports = (props) => {
     hideCancelButton,
     hideRetryButton,
     recoveredState,
+    uploadState,
+    totalProgress,
+    files,
+    supportsUploadProgress,
+    hideAfterFinish,
+    isSomeGhost,
+    isTargetDOMEl,
+    doneButtonHandler,
+    isUploadStarted,
+    i18n,
+    startUpload,
+    uppy,
+    isAllComplete,
+    showProgressDetails,
+    numUploads,
+    complete,
+    totalSize,
+    totalETA,
+    totalUploadedSize,
   } = props
 
-  const { uploadState } = props
+  function getProgressValue () {
+    switch (uploadState) {
+      case STATE_POSTPROCESSING:
+      case STATE_PREPROCESSING: {
+        const progress = calculateProcessingProgress(files)
 
-  let progressValue = props.totalProgress
-  let progressMode
-  let progressBarContent
+        if (progress.mode === 'determinate') {
+          return progress.value * 100
+        }
+        return totalProgress
+      }
+      case STATE_ERROR: {
+        return null
+      }
+      case STATE_UPLOADING: {
+        if (!supportsUploadProgress) {
+          return null
+        }
+        return totalProgress
+      }
+      default:
+        return totalProgress
+    }
+  }
 
-  if (uploadState === statusBarStates.STATE_PREPROCESSING || uploadState === statusBarStates.STATE_POSTPROCESSING) {
-    const progress = calculateProcessingProgress(props.files)
-    progressMode = progress.mode
-    if (progressMode === 'determinate') {
-      progressValue = progress.value * 100
+  function getIsIndeterminate () {
+    switch (uploadState) {
+      case STATE_POSTPROCESSING:
+      case STATE_PREPROCESSING: {
+        const { mode } = calculateProcessingProgress(files)
+        return mode === 'indeterminate'
+      }
+      case STATE_UPLOADING: {
+        if (!supportsUploadProgress) {
+          return true
+        }
+        return false
+      }
+      default:
+        return false
     }
+  }
 
-    progressBarContent = ProgressBarProcessing(progress)
-  } else if (uploadState === statusBarStates.STATE_COMPLETE) {
-    progressBarContent = ProgressBarComplete(props)
-  } else if (uploadState === statusBarStates.STATE_UPLOADING) {
-    if (!props.supportsUploadProgress) {
-      progressMode = 'indeterminate'
-      progressValue = null
+  function getIsHidden () {
+    if (recoveredState) {
+      return false
     }
 
-    progressBarContent = ProgressBarUploading(props)
-  } else if (uploadState === statusBarStates.STATE_ERROR) {
-    progressValue = undefined
-    progressBarContent = ProgressBarError(props)
+    switch (uploadState) {
+      case STATE_WAITING:
+        return hideUploadButton || newFiles === 0
+      case STATE_COMPLETE:
+        return hideAfterFinish
+      default:
+        return false
+    }
   }
 
-  const width = typeof progressValue === 'number' ? progressValue : 100
-  let isHidden = (uploadState === statusBarStates.STATE_WAITING && props.hideUploadButton)
-    || (uploadState === statusBarStates.STATE_WAITING && !props.newFiles > 0)
-    || (uploadState === statusBarStates.STATE_COMPLETE && props.hideAfterFinish)
+  const progressValue = getProgressValue()
 
-  let showUploadBtn = !error && newFiles
-    && !isUploadInProgress && !isAllPaused
-    && allowNewUpload && !hideUploadButton
+  const isHidden = getIsHidden()
 
-  if (recoveredState) {
-    isHidden = false
-    showUploadBtn = true
-  }
+  const width = progressValue ?? 100
+
+  const showUploadBtn
+    = !error
+    && newFiles
+    && !isUploadInProgress
+    && !isAllPaused
+    && allowNewUpload
+    && !hideUploadButton
 
-  const showCancelBtn = !hideCancelButton
-    && uploadState !== statusBarStates.STATE_WAITING
-    && uploadState !== statusBarStates.STATE_COMPLETE
-  const showPauseResumeBtn = resumableUploads && !hidePauseResumeButton
-    && uploadState === statusBarStates.STATE_UPLOADING
+  const showCancelBtn
+    = !hideCancelButton
+    && uploadState !== STATE_WAITING
+    && uploadState !== STATE_COMPLETE
+
+  const showPauseResumeBtn
+    = resumableUploads
+    && !hidePauseResumeButton
+    && uploadState === STATE_UPLOADING
 
   const showRetryBtn = error && !hideRetryButton
 
-  const showDoneBtn = props.doneButtonHandler && uploadState === statusBarStates.STATE_COMPLETE
+  const showDoneBtn = doneButtonHandler && uploadState === STATE_COMPLETE
 
-  const progressClassNames = `uppy-StatusBar-progress
-                           ${progressMode ? `is-${progressMode}` : ''}`
+  const progressClassNames = classNames('uppy-StatusBar-progress', {
+    'is-indeterminate': getIsIndeterminate(),
+  })
 
   const statusBarClassNames = classNames(
-    { 'uppy-Root': props.isTargetDOMEl },
+    { 'uppy-Root': isTargetDOMEl },
     'uppy-StatusBar',
     `is-${uploadState}`,
-    { 'has-ghosts': props.isSomeGhost }
+    { 'has-ghosts': isSomeGhost }
   )
 
   return (
@@ -140,291 +169,75 @@ module.exports = (props) => {
         aria-valuemax="100"
         aria-valuenow={progressValue}
       />
-      {progressBarContent}
-      <div className="uppy-StatusBar-actions">
-        {showUploadBtn ? <UploadBtn {...props} uploadState={uploadState} /> : null}
-        {showRetryBtn ? <RetryBtn {...props} /> : null}
-        {showPauseResumeBtn ? <PauseResumeButton {...props} /> : null}
-        {showCancelBtn ? <CancelBtn {...props} /> : null}
-        {showDoneBtn ? <DoneBtn {...props} /> : null}
-      </div>
-    </div>
-  )
-}
-
-const UploadBtn = (props) => {
-  const uploadBtnClassNames = classNames(
-    'uppy-u-reset',
-    'uppy-c-btn',
-    'uppy-StatusBar-actionBtn',
-    'uppy-StatusBar-actionBtn--upload',
-    { 'uppy-c-btn-primary': props.uploadState === statusBarStates.STATE_WAITING },
-    { 'uppy-StatusBar-actionBtn--disabled': props.isSomeGhost }
-  )
-
-  const uploadBtnText = props.newFiles && props.isUploadStarted && !props.recoveredState
-    ? props.i18n('uploadXNewFiles', { smart_count: props.newFiles })
-    : props.i18n('uploadXFiles', { smart_count: props.newFiles })
-
-  return (
-    <button
-      type="button"
-      className={uploadBtnClassNames}
-      aria-label={props.i18n('uploadXFiles', { smart_count: props.newFiles })}
-      onClick={props.startUpload}
-      disabled={props.isSomeGhost}
-      data-uppy-super-focusable
-    >
-      {uploadBtnText}
-    </button>
-  )
-}
-
-const RetryBtn = (props) => {
-  return (
-    <button
-      type="button"
-      className="uppy-u-reset uppy-c-btn uppy-StatusBar-actionBtn uppy-StatusBar-actionBtn--retry"
-      aria-label={props.i18n('retryUpload')}
-      onClick={() => props.uppy.retryAll()}
-      data-uppy-super-focusable
-    >
-      <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>
-      {props.i18n('retry')}
-    </button>
-  )
-}
 
-const CancelBtn = (props) => {
-  return (
-    <button
-      type="button"
-      className="uppy-u-reset uppy-StatusBar-actionCircleBtn"
-      title={props.i18n('cancel')}
-      aria-label={props.i18n('cancel')}
-      onClick={() => props.uppy.cancelAll()}
-      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>
-  )
-}
-
-const PauseResumeButton = (props) => {
-  const { isAllPaused, i18n } = props
-  const title = isAllPaused ? i18n('resume') : i18n('pause')
-
-  return (
-    <button
-      title={title}
-      aria-label={title}
-      className="uppy-u-reset uppy-StatusBar-actionCircleBtn"
-      type="button"
-      onClick={() => togglePauseResume(props)}
-      data-uppy-super-focusable
-    >
-      {isAllPaused ? (
-        <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="M6 4.25L11.5 8 6 11.75z" />
-          </g>
-        </svg>
-      ) : (
-        <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 d="M5 4.5h2v7H5v-7zm4 0h2v7H9v-7z" fill="#FFF" />
-          </g>
-        </svg>
-      )}
-    </button>
-  )
-}
-
-const DoneBtn = (props) => {
-  const { i18n } = props
-  return (
-    <button
-      type="button"
-      className="uppy-u-reset uppy-c-btn uppy-StatusBar-actionBtn uppy-StatusBar-actionBtn--done"
-      onClick={props.doneButtonHandler}
-      data-uppy-super-focusable
-    >
-      {i18n('done')}
-    </button>
-  )
-}
-
-const 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>
-  )
-}
-
-const ProgressBarProcessing = (props) => {
-  const value = Math.round(props.value * 100)
-
-  return (
-    <div className="uppy-StatusBar-content">
-      <LoadingSpinner />
-      {props.mode === 'determinate' ? `${value}% \u00B7 ` : ''}
-      {props.message}
-    </div>
-  )
-}
-
-const renderDot = () => ' \u00B7 '
-
-const ProgressDetails = (props) => {
-  const ifShowFilesUploadedOfTotal = props.numUploads > 1
-
-  return (
-    <div className="uppy-StatusBar-statusSecondary">
-      {
-        ifShowFilesUploadedOfTotal
-        && props.i18n('filesUploadedOfTotal', {
-          complete: props.complete,
-          smart_count: props.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()}
-
-        {
-          props.i18n('dataUploadedOfTotal', {
-            complete: prettierBytes(props.totalUploadedSize),
-            total: prettierBytes(props.totalSize),
-          })
+      {(() => {
+        switch (uploadState) {
+          case STATE_PREPROCESSING:
+          case STATE_POSTPROCESSING:
+            return (
+              <ProgressBarProcessing
+                progress={calculateProcessingProgress(files)}
+              />
+            )
+          case STATE_COMPLETE:
+            return <ProgressBarComplete i18n={i18n} />
+          case STATE_ERROR:
+            return <ProgressBarError error={error} i18n={i18n} />
+          case STATE_UPLOADING:
+            return (
+              <ProgressBarUploading
+                i18n={i18n}
+                supportsUploadProgress={supportsUploadProgress}
+                totalProgress={totalProgress}
+                showProgressDetails={showProgressDetails}
+                isUploadStarted={isUploadStarted}
+                isAllComplete={isAllComplete}
+                isAllPaused={isAllPaused}
+                newFiles={newFiles}
+                numUploads={numUploads}
+                complete={complete}
+                totalUploadedSize={totalUploadedSize}
+                totalSize={totalSize}
+                totalETA={totalETA}
+                startUpload={startUpload}
+              />
+            )
+          default:
+            return null
         }
+      })()}
 
-        {renderDot()}
-
-        {
-          props.i18n('xTimeLeft', {
-            time: prettyETA(props.totalETA),
-          })
-        }
-      </span>
-    </div>
-  )
-}
-
-const UnknownProgressDetails = (props) => {
-  return (
-    <div className="uppy-StatusBar-statusSecondary">
-      {props.i18n('filesUploadedOfTotal', { complete: props.complete, smart_count: props.numUploads })}
-    </div>
-  )
-}
-
-const UploadNewlyAddedFiles = (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">
-        {props.i18n('xMoreFilesAdded', { smart_count: props.newFiles })}
-      </div>
-      <button
-        type="button"
-        className={uploadBtnClassNames}
-        aria-label={props.i18n('uploadXFiles', { smart_count: props.newFiles })}
-        onClick={props.startUpload}
-      >
-        {props.i18n('upload')}
-      </button>
-    </div>
-  )
-}
-
-const ThrottledProgressDetails = throttle(ProgressDetails, 500, { leading: true, trailing: true })
-
-const ProgressBarUploading = (props) => {
-  if (!props.isUploadStarted || props.isAllComplete) {
-    return null
-  }
-
-  const title = props.isAllPaused ? props.i18n('paused') : props.i18n('uploading')
-  const showUploadNewlyAddedFiles = props.newFiles && props.isUploadStarted
-
-  return (
-    <div className="uppy-StatusBar-content" aria-label={title} title={title}>
-      {!props.isAllPaused ? <LoadingSpinner /> : null}
-      <div className="uppy-StatusBar-status">
-        <div className="uppy-StatusBar-statusPrimary">
-          {props.supportsUploadProgress ? `${title}: ${props.totalProgress}%` : title}
-        </div>
-        {/* eslint-disable-next-line no-nested-ternary */}
-        {!props.isAllPaused && !showUploadNewlyAddedFiles && props.showProgressDetails
-          ? (props.supportsUploadProgress ? <ThrottledProgressDetails {...props} /> : <UnknownProgressDetails {...props} />)
-          : null}
-        {showUploadNewlyAddedFiles ? <UploadNewlyAddedFiles {...props} /> : null}
-      </div>
-    </div>
-  )
-}
-
-const ProgressBarComplete = ({ i18n }) => {
-  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>
-  )
-}
-
-const ProgressBarError = ({ error, i18n }) => {
-  function displayErrorAlert () {
-    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" role="alert" title={i18n('uploadFailed')}>
-      <div className="uppy-StatusBar-status">
-        <div className="uppy-StatusBar-statusPrimary">
-          <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>
-          {i18n('uploadFailed')}
-        </div>
+      <div className="uppy-StatusBar-actions">
+        {(recoveredState || showUploadBtn) ? (
+          <UploadBtn
+            newFiles={newFiles}
+            isUploadStarted={isUploadStarted}
+            recoveredState={recoveredState}
+            i18n={i18n}
+            isSomeGhost={isSomeGhost}
+            startUpload={startUpload}
+            uploadState={uploadState}
+          />
+        ) : null}
+
+        {showRetryBtn ? <RetryBtn i18n={i18n} uppy={uppy} /> : null}
+
+        {showPauseResumeBtn ? (
+          <PauseResumeButton
+            isAllPaused={isAllPaused}
+            i18n={i18n}
+            isAllComplete={isAllComplete}
+            resumableUploads={resumableUploads}
+            uppy={uppy}
+          />
+        ) : null}
+
+        {showCancelBtn ? <CancelBtn i18n={i18n} uppy={uppy} /> : null}
+
+        {showDoneBtn ? (
+          <DoneBtn i18n={i18n} doneButtonHandler={doneButtonHandler} />
+        ) : null}
       </div>
-      <button
-        className="uppy-StatusBar-details"
-        aria-label={error}
-        data-microtip-position="top-right"
-        data-microtip-size="medium"
-        onClick={displayErrorAlert}
-        type="button"
-      >
-        ?
-      </button>
     </div>
   )
 }

+ 26 - 0
packages/@uppy/status-bar/src/calculateProcessingProgress.js

@@ -0,0 +1,26 @@
+module.export = function calculateProcessingProgress (files) {
+  const values = []
+  let mode
+  let message
+
+  for (const { progress } of Object.values(files)) {
+    const { preprocess, postprocess } = progress
+    // In the future we should probably do this differently. For now we'll take the
+    // mode and message from the first file…
+    if (message == null && (preprocess || postprocess)) {
+      ({ mode, message } = preprocess || postprocess)
+    }
+    if (preprocess?.mode === 'determinate') values.push(preprocess.value)
+    if (postprocess?.mode === 'determinate') values.push(postprocess.value)
+  }
+
+  const value = values.reduce((total, progressValue) => {
+    return total + progressValue / values.length
+  }, 0)
+
+  return {
+    mode,
+    message,
+    value,
+  }
+}

+ 79 - 67
packages/@uppy/status-bar/src/index.js

@@ -10,7 +10,8 @@ const StatusBarUI = require('./StatusBar')
  * progress percentage and time remaining.
  */
 module.exports = class StatusBar extends UIPlugin {
-  static VERSION = require('../package.json').version
+  // eslint-disable-next-line global-require
+  static VERSION = require('../package.json').version;
 
   constructor (uppy, opts) {
     super(uppy, opts)
@@ -72,74 +73,18 @@ module.exports = class StatusBar extends UIPlugin {
     this.install = this.install.bind(this)
   }
 
-  getTotalSpeed (files) {
-    let totalSpeed = 0
-    files.forEach((file) => {
-      totalSpeed += getSpeed(file.progress)
-    })
-    return totalSpeed
-  }
-
-  getTotalETA (files) {
-    const totalSpeed = this.getTotalSpeed(files)
-    if (totalSpeed === 0) {
-      return 0
-    }
-
-    const totalBytesRemaining = files.reduce((total, file) => {
-      return total + getBytesRemaining(file.progress)
-    }, 0)
-
-    return Math.round(totalBytesRemaining / totalSpeed * 10) / 10
-  }
-
   startUpload = () => {
     const { recoveredState } = this.uppy.getState()
+
     if (recoveredState) {
       this.uppy.emit('restore-confirmed')
-      return
+      return undefined
     }
+
     return this.uppy.upload().catch(() => {
       // Error logged in Core
     })
-  }
-
-  getUploadingState (isAllErrored, isAllComplete, recoveredState, files) {
-    if (isAllErrored) {
-      return statusBarStates.STATE_ERROR
-    }
-
-    if (isAllComplete) {
-      return statusBarStates.STATE_COMPLETE
-    }
-
-    if (recoveredState) {
-      return statusBarStates.STATE_WAITING
-    }
-
-    let state = statusBarStates.STATE_WAITING
-    const fileIDs = Object.keys(files)
-    for (let i = 0; i < fileIDs.length; i++) {
-      const { progress } = files[fileIDs[i]]
-      // If ANY files are being uploaded right now, show the uploading state.
-      if (progress.uploadStarted && !progress.uploadComplete) {
-        return statusBarStates.STATE_UPLOADING
-      }
-      // If files are being preprocessed AND postprocessed at this time, we show the
-      // preprocess state. If any files are being uploaded we show uploading.
-      if (progress.preprocess && state !== statusBarStates.STATE_UPLOADING) {
-        state = statusBarStates.STATE_PREPROCESSING
-      }
-      // If NO files are being preprocessed or uploaded right now, but some files are
-      // being postprocessed, show the postprocess state.
-      if (progress.postprocess
-          && state !== statusBarStates.STATE_UPLOADING
-          && state !== statusBarStates.STATE_PREPROCESSING) {
-        state = statusBarStates.STATE_POSTPROCESSING
-      }
-    }
-    return state
-  }
+  };
 
   render (state) {
     const {
@@ -168,8 +113,10 @@ module.exports = class StatusBar extends UIPlugin {
     // If some state was recovered, we want to show Upload button/counter
     // for all the files, because in this case it’s not an Upload button,
     // but “Confirm Restore Button”
-    const newFilesOrRecovered = recoveredState ? Object.values(files) : newFiles
-    const totalETA = this.getTotalETA(inProgressNotPausedFiles)
+    const newFilesOrRecovered = recoveredState
+      ? Object.values(files)
+      : newFiles
+    const totalETA = getTotalETA(inProgressNotPausedFiles)
     const resumableUploads = !!capabilities.resumableUploads
     const supportsUploadProgress = capabilities.uploadProgress !== false
 
@@ -177,18 +124,23 @@ module.exports = class StatusBar extends UIPlugin {
     let totalUploadedSize = 0
 
     startedFiles.forEach((file) => {
-      totalSize += (file.progress.bytesTotal || 0)
-      totalUploadedSize += (file.progress.bytesUploaded || 0)
+      totalSize += file.progress.bytesTotal || 0
+      totalUploadedSize += file.progress.bytesUploaded || 0
     })
 
     return StatusBarUI({
       error,
-      uploadState: this.getUploadingState(isAllErrored, isAllComplete, recoveredState, state.files || {}),
+      uploadState: getUploadingState(
+        isAllErrored,
+        isAllComplete,
+        recoveredState,
+        state.files || {}
+      ),
       allowNewUpload,
       totalProgress,
       totalSize,
       totalUploadedSize,
-      isAllComplete,
+      isAllComplete: false,
       isAllPaused,
       isAllErrored,
       isUploadStarted,
@@ -236,3 +188,63 @@ module.exports = class StatusBar extends UIPlugin {
     this.unmount()
   }
 }
+
+function getTotalSpeed (files) {
+  let totalSpeed = 0
+  files.forEach((file) => {
+    totalSpeed += getSpeed(file.progress)
+  })
+  return totalSpeed
+}
+
+function getTotalETA (files) {
+  const totalSpeed = getTotalSpeed(files)
+  if (totalSpeed === 0) {
+    return 0
+  }
+
+  const totalBytesRemaining = files.reduce((total, file) => {
+    return total + getBytesRemaining(file.progress)
+  }, 0)
+
+  return Math.round((totalBytesRemaining / totalSpeed) * 10) / 10
+}
+
+function getUploadingState (isAllErrored, isAllComplete, recoveredState, files) {
+  if (isAllErrored) {
+    return statusBarStates.STATE_ERROR
+  }
+
+  if (isAllComplete) {
+    return statusBarStates.STATE_COMPLETE
+  }
+
+  if (recoveredState) {
+    return statusBarStates.STATE_WAITING
+  }
+
+  let state = statusBarStates.STATE_WAITING
+  const fileIDs = Object.keys(files)
+  for (let i = 0; i < fileIDs.length; i++) {
+    const { progress } = files[fileIDs[i]]
+    // If ANY files are being uploaded right now, show the uploading state.
+    if (progress.uploadStarted && !progress.uploadComplete) {
+      return statusBarStates.STATE_UPLOADING
+    }
+    // If files are being preprocessed AND postprocessed at this time, we show the
+    // preprocess state. If any files are being uploaded we show uploading.
+    if (progress.preprocess && state !== statusBarStates.STATE_UPLOADING) {
+      state = statusBarStates.STATE_PREPROCESSING
+    }
+    // If NO files are being preprocessed or uploaded right now, but some files are
+    // being postprocessed, show the postprocess state.
+    if (
+      progress.postprocess
+      && state !== statusBarStates.STATE_UPLOADING
+      && state !== statusBarStates.STATE_PREPROCESSING
+    ) {
+      state = statusBarStates.STATE_POSTPROCESSING
+    }
+  }
+  return state
+}

+ 1 - 5
packages/@uppy/status-bar/src/style.scss

@@ -6,7 +6,7 @@
   position: relative;
   z-index: $zIndex-2;
   display: flex;
-  height: 40px;
+  height: 46px;
   color: $white;
   font-weight: 400;
   font-size: 12px;
@@ -14,10 +14,6 @@
   background-color: $white;
   transition: height 0.2s;
 
-  .uppy-size--md & {
-    height: 46px;
-  }
-
   [data-uppy-theme="dark"] & {
     background-color: $gray-900;
   }