Components.tsx 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544
  1. import type { Body, Meta } from '@uppy/utils/lib/UppyFile'
  2. import type { State, Uppy } from '@uppy/core/lib/Uppy'
  3. import type { FileProcessingInfo } from '@uppy/utils/lib/FileProgress'
  4. import type { I18n } from '@uppy/utils/lib/Translator'
  5. import { h } from 'preact'
  6. import classNames from 'classnames'
  7. import prettierBytes from '@transloadit/prettier-bytes'
  8. import prettyETA from '@uppy/utils/lib/prettyETA'
  9. import statusBarStates from './StatusBarStates.ts'
  10. const DOT = `\u00B7`
  11. const renderDot = (): string => ` ${DOT} `
  12. interface UploadBtnProps<M extends Meta, B extends Body> {
  13. newFiles: number
  14. isUploadStarted: boolean
  15. recoveredState: State<M, B>['recoveredState']
  16. i18n: I18n
  17. uploadState: string
  18. isSomeGhost: boolean
  19. startUpload: () => void
  20. }
  21. function UploadBtn<M extends Meta, B extends Body>(
  22. props: UploadBtnProps<M, B>,
  23. ) {
  24. const {
  25. newFiles,
  26. isUploadStarted,
  27. recoveredState,
  28. i18n,
  29. uploadState,
  30. isSomeGhost,
  31. startUpload,
  32. } = props
  33. const uploadBtnClassNames = classNames(
  34. 'uppy-u-reset',
  35. 'uppy-c-btn',
  36. 'uppy-StatusBar-actionBtn',
  37. 'uppy-StatusBar-actionBtn--upload',
  38. {
  39. 'uppy-c-btn-primary': uploadState === statusBarStates.STATE_WAITING,
  40. },
  41. { 'uppy-StatusBar-actionBtn--disabled': isSomeGhost },
  42. )
  43. const uploadBtnText =
  44. newFiles && isUploadStarted && !recoveredState ?
  45. i18n('uploadXNewFiles', { smart_count: newFiles })
  46. : i18n('uploadXFiles', { smart_count: newFiles })
  47. return (
  48. <button
  49. type="button"
  50. className={uploadBtnClassNames}
  51. aria-label={i18n('uploadXFiles', { smart_count: newFiles })}
  52. onClick={startUpload}
  53. disabled={isSomeGhost}
  54. data-uppy-super-focusable
  55. >
  56. {uploadBtnText}
  57. </button>
  58. )
  59. }
  60. interface RetryBtnProps<M extends Meta, B extends Body> {
  61. i18n: I18n
  62. uppy: Uppy<M, B>
  63. }
  64. function RetryBtn<M extends Meta, B extends Body>(props: RetryBtnProps<M, B>) {
  65. const { i18n, uppy } = props
  66. return (
  67. <button
  68. type="button"
  69. className="uppy-u-reset uppy-c-btn uppy-StatusBar-actionBtn uppy-StatusBar-actionBtn--retry"
  70. aria-label={i18n('retryUpload')}
  71. onClick={() =>
  72. uppy.retryAll().catch(() => {
  73. /* Error reported and handled via an event */
  74. })
  75. }
  76. data-uppy-super-focusable
  77. data-cy="retry"
  78. >
  79. <svg
  80. aria-hidden="true"
  81. focusable="false"
  82. className="uppy-c-icon"
  83. width="8"
  84. height="10"
  85. viewBox="0 0 8 10"
  86. >
  87. <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" />
  88. </svg>
  89. {i18n('retry')}
  90. </button>
  91. )
  92. }
  93. interface CancelBtnProps<M extends Meta, B extends Body> {
  94. i18n: I18n
  95. uppy: Uppy<M, B>
  96. }
  97. function CancelBtn<M extends Meta, B extends Body>(
  98. props: CancelBtnProps<M, B>,
  99. ) {
  100. const { i18n, uppy } = props
  101. return (
  102. <button
  103. type="button"
  104. className="uppy-u-reset uppy-StatusBar-actionCircleBtn"
  105. title={i18n('cancel')}
  106. aria-label={i18n('cancel')}
  107. onClick={(): void => uppy.cancelAll()}
  108. data-cy="cancel"
  109. data-uppy-super-focusable
  110. >
  111. <svg
  112. aria-hidden="true"
  113. focusable="false"
  114. className="uppy-c-icon"
  115. width="16"
  116. height="16"
  117. viewBox="0 0 16 16"
  118. >
  119. <g fill="none" fillRule="evenodd">
  120. <circle fill="#888" cx="8" cy="8" r="8" />
  121. <path
  122. fill="#FFF"
  123. 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"
  124. />
  125. </g>
  126. </svg>
  127. </button>
  128. )
  129. }
  130. interface PauseResumeButtonProps<M extends Meta, B extends Body> {
  131. i18n: I18n
  132. uppy: Uppy<M, B>
  133. isAllPaused: boolean
  134. isAllComplete: boolean
  135. resumableUploads: boolean
  136. }
  137. function PauseResumeButton<M extends Meta, B extends Body>(
  138. props: PauseResumeButtonProps<M, B>,
  139. ) {
  140. const { isAllPaused, i18n, isAllComplete, resumableUploads, uppy } = props
  141. const title = isAllPaused ? i18n('resume') : i18n('pause')
  142. function togglePauseResume(): void {
  143. if (isAllComplete) return
  144. if (!resumableUploads) {
  145. uppy.cancelAll()
  146. return
  147. }
  148. if (isAllPaused) {
  149. uppy.resumeAll()
  150. return
  151. }
  152. uppy.pauseAll()
  153. }
  154. return (
  155. <button
  156. title={title}
  157. aria-label={title}
  158. className="uppy-u-reset uppy-StatusBar-actionCircleBtn"
  159. type="button"
  160. onClick={togglePauseResume}
  161. data-cy="togglePauseResume"
  162. data-uppy-super-focusable
  163. >
  164. <svg
  165. aria-hidden="true"
  166. focusable="false"
  167. className="uppy-c-icon"
  168. width="16"
  169. height="16"
  170. viewBox="0 0 16 16"
  171. >
  172. <g fill="none" fillRule="evenodd">
  173. <circle fill="#888" cx="8" cy="8" r="8" />
  174. <path
  175. fill="#FFF"
  176. d={
  177. isAllPaused ?
  178. 'M6 4.25L11.5 8 6 11.75z'
  179. : 'M5 4.5h2v7H5v-7zm4 0h2v7H9v-7z'
  180. }
  181. />
  182. </g>
  183. </svg>
  184. </button>
  185. )
  186. }
  187. interface DoneBtnProps {
  188. i18n: I18n
  189. doneButtonHandler: (() => void) | undefined
  190. }
  191. function DoneBtn(props: DoneBtnProps) {
  192. const { i18n, doneButtonHandler } = props
  193. return (
  194. <button
  195. type="button"
  196. className="uppy-u-reset uppy-c-btn uppy-StatusBar-actionBtn uppy-StatusBar-actionBtn--done"
  197. onClick={doneButtonHandler}
  198. data-uppy-super-focusable
  199. >
  200. {i18n('done')}
  201. </button>
  202. )
  203. }
  204. function LoadingSpinner() {
  205. return (
  206. <svg
  207. className="uppy-StatusBar-spinner"
  208. aria-hidden="true"
  209. focusable="false"
  210. width="14"
  211. height="14"
  212. >
  213. <path
  214. 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"
  215. fillRule="evenodd"
  216. />
  217. </svg>
  218. )
  219. }
  220. interface ProgressBarProcessingProps {
  221. progress: FileProcessingInfo
  222. }
  223. function ProgressBarProcessing(props: ProgressBarProcessingProps) {
  224. const { progress } = props
  225. const { value, mode, message } = progress
  226. const dot = `\u00B7`
  227. return (
  228. <div className="uppy-StatusBar-content">
  229. <LoadingSpinner />
  230. {mode === 'determinate' ? `${Math.round(value * 100)}% ${dot} ` : ''}
  231. {message}
  232. </div>
  233. )
  234. }
  235. interface ProgressDetailsProps {
  236. i18n: I18n
  237. numUploads: number
  238. complete: number
  239. totalUploadedSize: number
  240. totalSize: number
  241. totalETA: number
  242. }
  243. function ProgressDetails(props: ProgressDetailsProps) {
  244. const { numUploads, complete, totalUploadedSize, totalSize, totalETA, i18n } =
  245. props
  246. const ifShowFilesUploadedOfTotal = numUploads > 1
  247. return (
  248. <div className="uppy-StatusBar-statusSecondary">
  249. {ifShowFilesUploadedOfTotal &&
  250. i18n('filesUploadedOfTotal', {
  251. complete,
  252. smart_count: numUploads,
  253. })}
  254. <span className="uppy-StatusBar-additionalInfo">
  255. {/* When should we render this dot?
  256. 1. .-additionalInfo is shown (happens only on desktops)
  257. 2. AND 'filesUploadedOfTotal' was shown
  258. */}
  259. {ifShowFilesUploadedOfTotal && renderDot()}
  260. {i18n('dataUploadedOfTotal', {
  261. complete: prettierBytes(totalUploadedSize),
  262. total: prettierBytes(totalSize),
  263. })}
  264. {renderDot()}
  265. {i18n('xTimeLeft', {
  266. time: prettyETA(totalETA),
  267. })}
  268. </span>
  269. </div>
  270. )
  271. }
  272. interface FileUploadCountProps {
  273. i18n: I18n
  274. complete: number
  275. numUploads: number
  276. }
  277. function FileUploadCount(props: FileUploadCountProps) {
  278. const { i18n, complete, numUploads } = props
  279. return (
  280. <div className="uppy-StatusBar-statusSecondary">
  281. {i18n('filesUploadedOfTotal', { complete, smart_count: numUploads })}
  282. </div>
  283. )
  284. }
  285. interface UploadNewlyAddedFilesProps {
  286. i18n: I18n
  287. newFiles: number
  288. startUpload: () => void
  289. }
  290. function UploadNewlyAddedFiles(props: UploadNewlyAddedFilesProps) {
  291. const { i18n, newFiles, startUpload } = props
  292. const uploadBtnClassNames = classNames(
  293. 'uppy-u-reset',
  294. 'uppy-c-btn',
  295. 'uppy-StatusBar-actionBtn',
  296. 'uppy-StatusBar-actionBtn--uploadNewlyAdded',
  297. )
  298. return (
  299. <div className="uppy-StatusBar-statusSecondary">
  300. <div className="uppy-StatusBar-statusSecondaryHint">
  301. {i18n('xMoreFilesAdded', { smart_count: newFiles })}
  302. </div>
  303. <button
  304. type="button"
  305. className={uploadBtnClassNames}
  306. aria-label={i18n('uploadXFiles', { smart_count: newFiles })}
  307. onClick={startUpload}
  308. >
  309. {i18n('upload')}
  310. </button>
  311. </div>
  312. )
  313. }
  314. interface ProgressBarUploadingProps {
  315. i18n: I18n
  316. supportsUploadProgress: boolean
  317. totalProgress: number
  318. showProgressDetails: boolean | undefined
  319. isUploadStarted: boolean
  320. isAllComplete: boolean
  321. isAllPaused: boolean
  322. newFiles: number
  323. numUploads: number
  324. complete: number
  325. totalUploadedSize: number
  326. totalSize: number
  327. totalETA: number
  328. startUpload: () => void
  329. }
  330. function ProgressBarUploading(props: ProgressBarUploadingProps) {
  331. const {
  332. i18n,
  333. supportsUploadProgress,
  334. totalProgress,
  335. showProgressDetails,
  336. isUploadStarted,
  337. isAllComplete,
  338. isAllPaused,
  339. newFiles,
  340. numUploads,
  341. complete,
  342. totalUploadedSize,
  343. totalSize,
  344. totalETA,
  345. startUpload,
  346. } = props
  347. const showUploadNewlyAddedFiles = newFiles && isUploadStarted
  348. if (!isUploadStarted || isAllComplete) {
  349. return null
  350. }
  351. const title = isAllPaused ? i18n('paused') : i18n('uploading')
  352. function renderProgressDetails() {
  353. if (!isAllPaused && !showUploadNewlyAddedFiles && showProgressDetails) {
  354. if (supportsUploadProgress) {
  355. return (
  356. <ProgressDetails
  357. numUploads={numUploads}
  358. complete={complete}
  359. totalUploadedSize={totalUploadedSize}
  360. totalSize={totalSize}
  361. totalETA={totalETA}
  362. i18n={i18n}
  363. />
  364. )
  365. }
  366. return (
  367. <FileUploadCount
  368. i18n={i18n}
  369. complete={complete}
  370. numUploads={numUploads}
  371. />
  372. )
  373. }
  374. return null
  375. }
  376. return (
  377. <div className="uppy-StatusBar-content" aria-label={title} title={title}>
  378. {!isAllPaused ?
  379. <LoadingSpinner />
  380. : null}
  381. <div className="uppy-StatusBar-status">
  382. <div className="uppy-StatusBar-statusPrimary">
  383. {supportsUploadProgress ? `${title}: ${totalProgress}%` : title}
  384. </div>
  385. {renderProgressDetails()}
  386. {showUploadNewlyAddedFiles ?
  387. <UploadNewlyAddedFiles
  388. i18n={i18n}
  389. newFiles={newFiles}
  390. startUpload={startUpload}
  391. />
  392. : null}
  393. </div>
  394. </div>
  395. )
  396. }
  397. interface ProgressBarCompleteProps {
  398. i18n: I18n
  399. }
  400. function ProgressBarComplete(props: ProgressBarCompleteProps) {
  401. const { i18n } = props
  402. return (
  403. <div
  404. className="uppy-StatusBar-content"
  405. role="status"
  406. title={i18n('complete')}
  407. >
  408. <div className="uppy-StatusBar-status">
  409. <div className="uppy-StatusBar-statusPrimary">
  410. <svg
  411. aria-hidden="true"
  412. focusable="false"
  413. className="uppy-StatusBar-statusIndicator uppy-c-icon"
  414. width="15"
  415. height="11"
  416. viewBox="0 0 15 11"
  417. >
  418. <path d="M.414 5.843L1.627 4.63l3.472 3.472L13.202 0l1.212 1.213L5.1 10.528z" />
  419. </svg>
  420. {i18n('complete')}
  421. </div>
  422. </div>
  423. </div>
  424. )
  425. }
  426. interface ProgressBarErrorProps {
  427. i18n: I18n
  428. error: any
  429. complete: number
  430. numUploads: number
  431. }
  432. function ProgressBarError(props: ProgressBarErrorProps) {
  433. const { error, i18n, complete, numUploads } = props
  434. function displayErrorAlert(): void {
  435. const errorMessage = `${i18n('uploadFailed')} \n\n ${error}`
  436. // eslint-disable-next-line no-alert
  437. alert(errorMessage) // TODO: move to custom alert implementation
  438. }
  439. return (
  440. <div className="uppy-StatusBar-content" title={i18n('uploadFailed')}>
  441. <svg
  442. aria-hidden="true"
  443. focusable="false"
  444. className="uppy-StatusBar-statusIndicator uppy-c-icon"
  445. width="11"
  446. height="11"
  447. viewBox="0 0 11 11"
  448. >
  449. <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" />
  450. </svg>
  451. <div className="uppy-StatusBar-status">
  452. <div className="uppy-StatusBar-statusPrimary">
  453. {i18n('uploadFailed')}
  454. <button
  455. className="uppy-u-reset uppy-StatusBar-details"
  456. aria-label={i18n('showErrorDetails')}
  457. data-microtip-position="top-right"
  458. data-microtip-size="medium"
  459. onClick={displayErrorAlert}
  460. type="button"
  461. >
  462. ?
  463. </button>
  464. </div>
  465. <FileUploadCount
  466. i18n={i18n}
  467. complete={complete}
  468. numUploads={numUploads}
  469. />
  470. </div>
  471. </div>
  472. )
  473. }
  474. export {
  475. UploadBtn,
  476. RetryBtn,
  477. CancelBtn,
  478. PauseResumeButton,
  479. DoneBtn,
  480. LoadingSpinner,
  481. ProgressDetails,
  482. ProgressBarProcessing,
  483. ProgressBarError,
  484. ProgressBarUploading,
  485. ProgressBarComplete,
  486. }