import React, { Component } from 'react'; import PropTypes from 'prop-types'; import { defineMessages, injectIntl } from 'react-intl'; import { TAB } from '/imports/utils/keyCodes'; import deviceInfo from '/imports/utils/deviceInfo'; import Button from '/imports/ui/components/common/button/component'; import update from 'immutability-helper'; import logger from '/imports/startup/client/logger'; import { toast } from 'react-toastify'; import { registerTitleView, unregisterTitleView } from '/imports/utils/dom-utils'; import Styled from './styles'; import PresentationDownloadDropdown from './presentation-download-dropdown/component'; import { getSettingsSingletonInstance } from '/imports/ui/services/settings'; import Radio from '/imports/ui/components/common/radio/component'; import { unique } from 'radash'; import { isPresentationEnabled } from '/imports/ui/services/features'; const { isMobile } = deviceInfo; const propTypes = { allowDownloadOriginal: PropTypes.bool.isRequired, allowDownloadConverted: PropTypes.bool.isRequired, allowDownloadWithAnnotations: PropTypes.bool.isRequired, intl: PropTypes.shape({ formatMessage: PropTypes.func.isRequired, }).isRequired, fileUploadConstraintsHint: PropTypes.bool.isRequired, fileSizeMax: PropTypes.number.isRequired, filePagesMax: PropTypes.number.isRequired, handleSave: PropTypes.func.isRequired, dispatchChangePresentationDownloadable: PropTypes.func.isRequired, fileValidMimeTypes: PropTypes.arrayOf(PropTypes.shape).isRequired, presentations: PropTypes.arrayOf(PropTypes.shape({ presentationId: PropTypes.string.isRequired, name: PropTypes.string.isRequired, current: PropTypes.bool.isRequired, })).isRequired, currentPresentation: PropTypes.string.isRequired, isOpen: PropTypes.bool.isRequired, handleFiledrop: PropTypes.func.isRequired, selectedToBeNextCurrent: PropTypes.string, renderPresentationItemStatus: PropTypes.func.isRequired, externalUploadData: PropTypes.shape({ presentationUploadExternalDescription: PropTypes.string.isRequired, presentationUploadExternalUrl: PropTypes.string.isRequired, }).isRequired, isPresenter: PropTypes.bool.isRequired, exportPresentation: PropTypes.func.isRequired, }; const defaultProps = { selectedToBeNextCurrent: '', }; const intlMessages = defineMessages({ currentBadge: { id: 'app.presentationUploder.currentBadge', }, title: { id: 'app.presentationUploder.title', description: 'title of the modal', }, message: { id: 'app.presentationUploder.message', description: 'message warning the types of files accepted', }, uploadLabel: { id: 'app.presentationUploder.uploadLabel', description: 'confirm label when presentations are to be uploaded', }, confirmLabel: { id: 'app.presentationUploder.confirmLabel', description: 'confirm label when no presentations are to be uploaded', }, confirmDesc: { id: 'app.presentationUploder.confirmDesc', description: 'description of the confirm', }, dismissLabel: { id: 'app.presentationUploder.dismissLabel', description: 'used in the button that close modal', }, dismissDesc: { id: 'app.presentationUploder.dismissDesc', description: 'description of the dismiss', }, dropzoneLabel: { id: 'app.presentationUploder.dropzoneLabel', description: 'message warning where drop files for upload', }, externalUploadTitle: { id: 'app.presentationUploder.externalUploadTitle', description: 'title for external upload area', }, externalUploadLabel: { id: 'app.presentationUploder.externalUploadLabel', description: 'message of external upload button', }, dropzoneImagesLabel: { id: 'app.presentationUploder.dropzoneImagesLabel', description: 'message warning where drop images for upload', }, browseFilesLabel: { id: 'app.presentationUploder.browseFilesLabel', description: 'message use on the file browser', }, browseImagesLabel: { id: 'app.presentationUploder.browseImagesLabel', description: 'message use on the image browser', }, fileToUpload: { id: 'app.presentationUploder.fileToUpload', description: 'message used in the file selected for upload', }, extraHint: { id: 'app.presentationUploder.extraHint', description: 'message used to indicate upload file max sizes', }, rejectedError: { id: 'app.presentationUploder.rejectedError', description: 'some files rejected, please check the file mime types', }, badConnectionError: { id: 'app.presentationUploder.connectionClosedError', description: 'message indicating that the connection was closed', }, uploadProcess: { id: 'app.presentationUploder.upload.progress', description: 'message that indicates the percentage of the upload', }, 413: { id: 'app.presentationUploder.upload.413', description: 'error that file exceed the size limit', }, 408: { id: 'app.presentationUploder.upload.408', description: 'error for token request timeout', }, 404: { id: 'app.presentationUploder.upload.404', description: 'error not found', }, 401: { id: 'app.presentationUploder.upload.401', description: 'error for failed upload token request.', }, conversionProcessingSlides: { id: 'app.presentationUploder.conversion.conversionProcessingSlides', description: 'indicates how many slides were converted', }, genericError: { id: 'app.presentationUploder.genericError', description: 'generic error while uploading/converting', }, genericConversionStatus: { id: 'app.presentationUploder.conversion.genericConversionStatus', description: 'indicates that file is being converted', }, TIMEOUT: { id: 'app.presentationUploder.conversion.timeout', }, CONVERSION_TIMEOUT: { id: 'app.presentationUploder.conversion.conversionTimeout', description: 'warns the user that the presentation timed out in the back-end in specific page of the document', }, GENERATING_THUMBNAIL: { id: 'app.presentationUploder.conversion.generatingThumbnail', description: 'indicatess that it is generating thumbnails', }, GENERATING_SVGIMAGES: { id: 'app.presentationUploder.conversion.generatingSvg', description: 'warns that it is generating svg images', }, GENERATED_SLIDE: { id: 'app.presentationUploder.conversion.generatedSlides', description: 'warns that were slides generated', }, PAGE_COUNT_EXCEEDED: { id: 'app.presentationUploder.conversion.pageCountExceeded', description: 'warns the user that the conversion failed because of the page count', }, PDF_HAS_BIG_PAGE: { id: 'app.presentationUploder.conversion.pdfHasBigPage', description: 'warns the user that the conversion failed because of the pdf page siz that exceeds the allowed limit', }, OFFICE_DOC_CONVERSION_INVALID: { id: 'app.presentationUploder.conversion.officeDocConversionInvalid', description: '', }, OFFICE_DOC_CONVERSION_FAILED: { id: 'app.presentationUploder.conversion.officeDocConversionFailed', description: 'warns the user that the conversion failed because of wrong office file', }, UNSUPPORTED_DOCUMENT: { id: 'app.presentationUploder.conversion.unsupportedDocument', description: 'warns the user that the file extension is not supported', }, isDownloadable: { id: 'app.presentationUploder.isDownloadableLabel', description: 'presentation is available for downloading by all viewers', }, isNotDownloadable: { id: 'app.presentationUploder.isNotDownloadableLabel', description: 'presentation is not available for downloading the viewers', }, removePresentation: { id: 'app.presentationUploder.removePresentationLabel', description: 'select to delete this presentation', }, setAsCurrentPresentation: { id: 'app.presentationUploder.setAsCurrentPresentation', description: 'set this presentation to be the current one', }, status: { id: 'app.presentationUploder.tableHeading.status', description: 'aria label status table heading', }, options: { id: 'app.presentationUploder.tableHeading.options', description: 'aria label for options table heading', }, filename: { id: 'app.presentationUploder.tableHeading.filename', description: 'aria label for file name table heading', }, uploading: { id: 'app.presentationUploder.uploading', description: 'uploading label for toast notification', }, uploadStatus: { id: 'app.presentationUploder.uploadStatus', description: 'upload status for toast notification', }, completed: { id: 'app.presentationUploder.completed', description: 'uploads complete label for toast notification', }, item: { id: 'app.presentationUploder.item', description: 'single item label', }, itemPlural: { id: 'app.presentationUploder.itemPlural', description: 'plural item label', }, clearErrors: { id: 'app.presentationUploder.clearErrors', description: 'button label for clearing upload errors', }, clearErrorsDesc: { id: 'app.presentationUploder.clearErrorsDesc', description: 'aria description for button clearing upload error', }, uploadViewTitle: { id: 'app.presentationUploder.uploadViewTitle', description: 'view name apended to document title', }, exportHint: { id: 'app.presentationUploader.exportHint', description: 'message to indicate the export presentation option', }, exportToastHeader: { id: 'app.presentationUploader.exportToastHeader', description: 'exporting toast header', }, exportToastHeaderPlural: { id: 'app.presentationUploader.exportToastHeaderPlural', description: 'exporting toast header in plural', }, export: { id: 'app.presentationUploader.export', description: 'send presentation to chat', }, exporting: { id: 'app.presentationUploader.exporting', description: 'presentation is being sent to chat', }, currentLabel: { id: 'app.presentationUploader.currentPresentationLabel', description: 'current presentation label', }, actionsLabel: { id: 'app.presentation.actionsLabel', description: 'actions label', }, }); const handleDismissToast = (id) => toast.dismiss(id); class PresentationUploader extends Component { constructor(props) { super(props); this.state = { presentations: [], disableActions: false, presExporting: new Set(), }; this.toastId = null; this.hasError = null; this.exportToastId = null; const { handleFiledrop } = this.props; // handlers this.handleFiledrop = handleFiledrop; this.handleConfirm = this.handleConfirm.bind(this); this.handleDismiss = this.handleDismiss.bind(this); this.handleRemove = this.handleRemove.bind(this); this.handleCurrentChange = this.handleCurrentChange.bind(this); this.handleDownloadingOfPresentation = this.handleDownloadingOfPresentation.bind(this); // renders this.renderDropzone = this.renderDropzone.bind(this); this.renderExternalUpload = this.renderExternalUpload.bind(this); this.renderPicDropzone = this.renderPicDropzone.bind(this); this.renderPresentationList = this.renderPresentationList.bind(this); this.renderPresentationItem = this.renderPresentationItem.bind(this); // utilities this.deepMergeUpdateFileKey = this.deepMergeUpdateFileKey.bind(this); this.updateFileKey = this.updateFileKey.bind(this); this.getPresentationsToShow = this.getPresentationsToShow.bind(this); this.handleDownloadableChange = this.handleDownloadableChange.bind(this); } componentDidUpdate(prevProps) { const { isOpen, presentations: propPresentations, currentPresentation, intl } = this.props; const { presentations } = this.state; const { presentations: prevPropPresentations } = prevProps; let shouldUpdateState = isOpen && !prevProps.isOpen; const presState = Object.values({ ...JSON.parse(JSON.stringify(propPresentations)), ...JSON.parse(JSON.stringify(presentations)), }); if (propPresentations.length > prevPropPresentations.length) { shouldUpdateState = true; const propsDiffs = propPresentations.filter( (p) => !prevPropPresentations.some( (presentation) => p.presentationId === presentation.presentationId || p.temporaryPresentationId === presentation.temporaryPresentationId, ), ); propsDiffs.forEach((p) => { const index = presState.findIndex( (pres) => pres.temporaryPresentationId === p.temporaryPresentationId || pres.presentationId === p.presentationId, ); if (index === -1) { presState.push(p); } }); } const presStateFiltered = presState.filter((presentation) => { const currentPropPres = propPresentations.find((pres) => pres.presentationId === presentation.presentationId); const prevPropPres = prevPropPresentations.find((pres) => pres.presentationId === presentation.presentationId); const hasConversionError = !!presentation?.uploadErrorMsgKey; const finishedConversion = !presentation?.uploadInProgress || !currentPropPres?.uploadInProgress; const hasTemporaryId = presentation.presentationId.startsWith(presentation.name); if (hasConversionError || (!finishedConversion && hasTemporaryId)) return true; if (!currentPropPres) return false; const modPresentation = presentation; if (currentPropPres.current !== prevPropPres?.current) { modPresentation.current = currentPropPres.current; shouldUpdateState = true; } if (currentPropPres?.totalPagesUploaded !== prevPropPres?.totalPagesUploaded || presentation.totalPagesUploaded !== currentPropPres?.totalPagesUploaded) { modPresentation.totalPagesUploaded = currentPropPres.totalPagesUploaded; shouldUpdateState = true; } if (currentPropPres?.uploadCompleted !== prevPropPres?.uploadCompleted || presentation.uploadCompleted !== currentPropPres?.uploadCompleted) { modPresentation.uploadCompleted = currentPropPres.uploadCompleted; shouldUpdateState = true; } if (currentPropPres?.totalPages !== prevPropPres?.totalPages || presentation.totalPages !== currentPropPres?.totalPages) { modPresentation.totalPages = currentPropPres.totalPages; shouldUpdateState = true; } if (currentPropPres?.downloadable !== prevPropPres?.downloadable) { presentation.downloadable = currentPropPres.downloadable; shouldUpdateState = true; } if (currentPropPres?.downloadFileUri !== prevPropPres?.downloadFileUri) { presentation.downloadFileUri = currentPropPres.downloadFileUri; shouldUpdateState = true; } if (currentPropPres?.filenameConverted !== prevPropPres?.filenameConverted) { presentation.filenameConverted = currentPropPres.filenameConverted; shouldUpdateState = true; } modPresentation.uploadInProgress = currentPropPres.uploadInProgress; modPresentation.removable = currentPropPres.removable; return true; }).filter((presentation) => { const duplicated = presentations.find( (pres) => pres.name === presentation.name && pres.presentationId !== presentation.presentationId, ); if (duplicated && duplicated.presentationId.startsWith(presentation.name) && !presentation.presentationId.startsWith(presentation.name) && presentation?.uploadInProgress === duplicated?.uploadInProgress) { return false; // Prioritizing propPresentations (the one with id from back-end) } return true; }); if (shouldUpdateState) { this.setState({ presentations: unique(presStateFiltered, p => p.presentationId) }); } if (!isOpen && prevProps.isOpen) { unregisterTitleView(); } // Updates presentation list when modal opens to avoid missing presentations if (isOpen && !prevProps.isOpen) { registerTitleView(intl.formatMessage(intlMessages.uploadViewTitle)); const focusableElements = 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'; const modal = document.getElementById('upload-modal'); const firstFocusableElement = modal?.querySelectorAll(focusableElements)[0]; const focusableContent = modal?.querySelectorAll(focusableElements); const lastFocusableElement = focusableContent[focusableContent.length - 1]; firstFocusableElement.focus(); modal.addEventListener('keydown', (e) => { const tab = e.key === 'Tab' || e.keyCode === TAB; if (!tab) return; if (e.shiftKey) { if (document.activeElement === firstFocusableElement) { lastFocusableElement.focus(); e.preventDefault(); } } else if (document.activeElement === lastFocusableElement) { firstFocusableElement.focus(); e.preventDefault(); } }); } if (currentPresentation && currentPresentation !== prevProps.currentPresentation) { this.handleCurrentChange(currentPresentation); } if (presentations.length > 0) { const selected = propPresentations.filter((p) => p.current); if (selected.length > 0) Session.set('selectedToBeNextCurrent', selected[0].presentationId); } if (this.exportToastId) { if (!prevProps.isOpen && isOpen) { handleDismissToast(this.exportToastId); } toast.update(this.exportToastId, { render: this.renderExportToast(), }); } } componentWillUnmount() { const id = Session.get('presentationUploaderToastId'); if (id) { toast.dismiss(id); Session.set('presentationUploaderToastId', null); } Session.set('showUploadPresentationView', false); } handleRemove(item, withErr = false) { if (withErr) { const { presentations } = this.state; const { presentations: propPresentations } = this.props; const filteredPropPresentations = propPresentations.filter(d => d.uploadCompleted && !d.uploadInProgress); const ids = new Set(filteredPropPresentations.map((d) => d.presentationId)); const filteredPresentations = presentations.filter((d) => { d.current = false; return !ids.has(d.presentationId) && !d.uploadErrorMsgKey && !(d.uploadCompleted && !d.uploadInProgress)}); const merged = [ ...filteredPresentations, ...filteredPropPresentations, ]; let hasUploading merged.forEach(d => { if (!d.uploadCompleted || d.uploadInProgress) { hasUploading = true; }}) this.hasError = false; if (hasUploading) { return this.setState({ presentations: merged, }); } else { return this.setState({ presentations: merged, disableActions: false, }); } } const { presentations } = this.state; const toRemoveIndex = presentations.indexOf(item); return this.setState({ presentations: update(presentations, { $splice: [[toRemoveIndex, 1]], }), }, () => { const { presentations: updatedPresentations, oldCurrentId } = this.state; const commands = {}; const currentIndex = updatedPresentations.findIndex((p) => p.current); const actualCurrentIndex = updatedPresentations.findIndex((p) => p.presentationId === oldCurrentId); if (currentIndex === -1 && updatedPresentations.length > 0) { const newCurrentIndex = actualCurrentIndex === -1 ? 0 : actualCurrentIndex; commands[newCurrentIndex] = { $apply: (presentation) => { const p = presentation; p.current = true; return p; }, }; } const updatedCurrent = update(updatedPresentations, commands); this.setState({ presentations: updatedCurrent }); }); } handleCurrentChange(id) { const { presentations, disableActions } = this.state; if (disableActions || presentations?.length === 0) return; const currentIndex = presentations.findIndex((p) => p.current); const newCurrentIndex = presentations.findIndex((p) => p.presentationId === id); const commands = {}; // we can end up without a current presentation if (currentIndex !== -1) { commands[currentIndex] = { $apply: (presentation) => { const p = presentation; p.current = false; return p; }, }; } commands[newCurrentIndex] = { $apply: (presentation) => { if (!presentation) return; const p = presentation; if (p) { p.current = true; } return p; }, }; const presentationsUpdated = update(presentations, commands); this.setState({ presentations: presentationsUpdated }); } handleConfirm() { const { handleSave, selectedToBeNextCurrent, presentations: propPresentations, dispatchChangePresentationDownloadable, setPresentation, removePresentation, } = this.props; const { disableActions, presentations } = this.state; const presentationsToSave = presentations; if (!isPresentationEnabled()) { this.setState( { presentations: [] }, Session.set('showUploadPresentationView', false), ); return null; } this.setState({ disableActions: true }); presentations.forEach((item) => { if (item.uploadCompleted) { const didDownloadableStateChange = propPresentations.some( (p) => p.presentationId === item.presentationId && p.downloadable !== item.downloadable, ); if (didDownloadableStateChange) { dispatchChangePresentationDownloadable(item, item.downloadable); } } }); if (!disableActions) { Session.set('showUploadPresentationView', false); return handleSave( presentationsToSave, true, {}, propPresentations, setPresentation, removePresentation, ) .then(() => { const hasError = presentations.some((p) => !!p.uploadErrorMsgKey); if (!hasError) { this.setState({ disableActions: false, }); return; } // if there's error we don't want to close the modal this.setState({ disableActions: true, // preventClosing: true, }, () => { // if the selected current has error we revert back to the old one const newCurrent = presentations.find((p) => p.current); if (newCurrent.uploadErrorMsgKey) { this.handleCurrentChange(selectedToBeNextCurrent); } }); }) .catch((error) => { logger.error({ logCode: 'presentationuploader_component_save_error', extraInfo: { error }, }, 'Presentation uploader catch error on confirm'); }); } Session.set('showUploadPresentationView', false); return null; } handleDownloadableChange(item, fileStateType, downloadable) { const { dispatchChangePresentationDownloadable } = this.props; dispatchChangePresentationDownloadable(item, downloadable, fileStateType); } handleDismiss() { const { presentations } = this.state; const { presentations: propPresentations } = this.props; const ids = new Set(propPresentations.map((d) => d.presentationId)); const filteredPresentations = presentations.filter((d) => !ids.has(d.presentationId) && (d.uploadCompleted || d.totalPagesUploaded !== 0)); const isThereStateCurrentPres = filteredPresentations.some((p) => p.current); const merged = [ ...filteredPresentations, ...propPresentations.filter((p) => { if (isThereStateCurrentPres) { p.current = false; } return true; }), ]; this.setState( { presentations: merged }, Session.set('showUploadPresentationView', false), ); } handleDownloadingOfPresentation(item, fileStateType) { const { exportPresentation } = this.props; exportPresentation(item.presentationId, fileStateType); } getPresentationsToShow() { const { presentations, presExporting } = this.state; return Array.from(presExporting) .map((id) => presentations.find((p) => p.presentationId === id)) .filter((p) => p); } deepMergeUpdateFileKey(id, key, value) { const applyValue = (toUpdate) => update(toUpdate, { $merge: value }); this.updateFileKey(id, key, applyValue, '$apply'); } updateFileKey(id, key, value, operation = '$set') { this.setState(({ presentations }) => { const fileIndex = presentations.findIndex((f) => f.presentationId === id); return fileIndex === -1 ? false : { presentations: update(presentations, { [fileIndex]: { $apply: (file) => update(file, { [key]: { [operation]: value, }, }), }, }), }; }); } renderExtraHint() { const { intl, fileSizeMax, filePagesMax, } = this.props; const options = { 0: fileSizeMax / 1000000, 1: filePagesMax, }; return ( {intl.formatMessage(intlMessages.extraHint, options)} ); } renderPresentationList() { const { presentations } = this.state; const { intl } = this.props; let presentationsSorted = presentations; try { presentationsSorted = presentations .sort((a, b) => a.uploadTimestamp - b.uploadTimestamp) .sort((a, b) => a.name.localeCompare(b.name)) .sort((a, b) => b.totalPagesUploaded - a.totalPagesUploaded) .sort((a, b) => b.uploadInProgress - a.uploadInProgress) .sort((a, b) => { const aUploadNotTriggeredYet = !a.uploadCompleted && a.totalPagesUploaded === 0; const bUploadNotTriggeredYet = !b.uploadCompleted && b.totalPagesUploaded === 0; return bUploadNotTriggeredYet - aUploadNotTriggeredYet; }); } catch (error) { logger.error({ logCode: 'presentationuploader_component_render_error', extraInfo: { error }, }, 'Presentation uploader catch error on render presentation list'); } return ( {intl.formatMessage(intlMessages.setAsCurrentPresentation)} {intl.formatMessage(intlMessages.filename)} {intl.formatMessage(intlMessages.status)} {intl.formatMessage(intlMessages.options)} {intl.formatMessage(intlMessages.currentLabel)} {intl.formatMessage(intlMessages.actionsLabel)} {unique(presentationsSorted, p => p.presentationId) .map((item) => this.renderPresentationItem(item))} ); } renderDownloadableWithAnnotationsHint() { const { intl, allowDownloadWithAnnotations, } = this.props; return allowDownloadWithAnnotations ? ( {intl.formatMessage(intlMessages.exportHint)} ) : null; } renderPresentationItem(item) { const { disableActions } = this.state; const { intl, selectedToBeNextCurrent, allowDownloadOriginal, allowDownloadConverted, allowDownloadWithAnnotations, renderPresentationItemStatus, } = this.props; const isActualCurrent = selectedToBeNextCurrent ? item.presentationId === selectedToBeNextCurrent : item.current; const isUploading = !item.uploadCompleted; const uploadInProgress = item.uploadInProgress; const hasError = !!item.uploadErrorMsgKey; const isProcessing = (isUploading || uploadInProgress) && !hasError; if (hasError) { this.hasError = true; } const Settings = getSettingsSingletonInstance(); const { animations } = Settings.application; const { removable, downloadable } = item; const isExporting = item?.exportToChatStatus === 'RUNNING'; const shouldDisableExportButton = (isExporting || !item.uploadCompleted || hasError || disableActions); const formattedDownloadLabel = isExporting ? intl.formatMessage(intlMessages.exporting) : intl.formatMessage(intlMessages.export); const formattedDownloadAriaLabel = `${formattedDownloadLabel} ${item.name}`; return ( this.handleCurrentChange(item.presentationId)} disabled={disableActions || hasError} /> {item.name} { isActualCurrent ? ( {intl.formatMessage(intlMessages.currentBadge)} ) : null } {renderPresentationItemStatus(item, intl)} { hasError ? null : ( {allowDownloadOriginal || allowDownloadWithAnnotations || allowDownloadConverted ? ( Session.set('showUploadPresentationView', false)} handleDownloadingOfPresentation={(fileStateType) => this .handleDownloadingOfPresentation(item, fileStateType)} /> ) : null} {removable ? ( this.handleRemove(item)} animations={animations} /> ) : null} )} ); } renderDropzone() { const { intl, fileValidMimeTypes, } = this.props; const { disableActions } = this.state; if (disableActions && !this.hasError) return null; return this.hasError ? (
) : ( // Until the Dropzone package has fixed the mime type hover validation, the rejectClassName // prop is being remove to prevent the error styles from being applied to valid file types. // Error handling is being done in the onDrop prop. fileValid.extension)} disablepreview="true" onDrop={(files, files2) => this.handleFiledrop(files, files2, this, intl, intlMessages)} > {intl.formatMessage(intlMessages.dropzoneLabel)}   {intl.formatMessage(intlMessages.browseFilesLabel)} ); } renderExternalUpload() { const { externalUploadData, intl } = this.props; const { presentationUploadExternalDescription, presentationUploadExternalUrl, } = externalUploadData; if (!presentationUploadExternalDescription || !presentationUploadExternalUrl) return null; return (
{intl.formatMessage(intlMessages.externalUploadTitle)}

{presentationUploadExternalDescription}

window.open(`${presentationUploadExternalUrl}`)} label={intl.formatMessage(intlMessages.externalUploadLabel)} aria-describedby={intl.formatMessage(intlMessages.externalUploadLabel)} />
); } renderPicDropzone() { const { intl, } = this.props; const { disableActions } = this.state; if (disableActions && !this.hasError) return null; return this.hasError ? (
) : ( this.handleFiledrop(files, files2, this, intl, intlMessages)} > {intl.formatMessage(intlMessages.dropzoneImagesLabel)}   {intl.formatMessage(intlMessages.browseImagesLabel)} ); } render() { const { isOpen, isPresenter, intl, fileUploadConstraintsHint, } = this.props; if (!isPresenter) return null; const { presentations, disableActions } = this.state; let hasNewUpload = false; presentations.forEach((item) => { if (item?.presentationId.indexOf(item.name) !== -1 && item.totalPagesUploaded === 0) hasNewUpload = true; }); return ( <> {isOpen ? ( {intl.formatMessage(intlMessages.title)} this.handleConfirm()} disabled={disableActions} label={hasNewUpload ? intl.formatMessage(intlMessages.uploadLabel) : intl.formatMessage(intlMessages.confirmLabel)} /> {`${intl.formatMessage(intlMessages.message)}`} {fileUploadConstraintsHint ? this.renderExtraHint() : null} {this.renderPresentationList()} {this.renderDownloadableWithAnnotationsHint()} {isMobile ? this.renderPicDropzone() : null} {this.renderDropzone()} {this.renderExternalUpload()} ) : null} ); } } PresentationUploader.propTypes = propTypes; PresentationUploader.defaultProps = defaultProps; export default injectIntl(PresentationUploader);