bigbluebutton-Github/bigbluebutton-html5/imports/ui/components/presentation/presentation-uploader/component.jsx

1015 lines
33 KiB
React
Raw Normal View History

2017-04-29 02:42:32 +08:00
import React, { Component } from 'react';
2017-09-08 02:18:14 +08:00
import PropTypes from 'prop-types';
import { defineMessages, injectIntl } from 'react-intl';
2020-02-26 03:29:14 +08:00
import cx from 'classnames';
2021-04-01 01:13:36 +08:00
import deviceInfo from '/imports/utils/deviceInfo';
2020-02-26 03:29:14 +08:00
import Button from '/imports/ui/components/button/component';
import Checkbox from '/imports/ui/components/checkbox/component';
import Icon from '/imports/ui/components/icon/component';
import Dropzone from 'react-dropzone';
2017-05-04 00:36:16 +08:00
import update from 'immutability-helper';
import logger from '/imports/startup/client/logger';
2020-02-26 03:29:14 +08:00
import { notify } from '/imports/ui/services/notification';
import { toast } from 'react-toastify';
import _ from 'lodash';
2020-02-26 03:29:14 +08:00
import { styles } from './styles';
2021-04-01 01:13:36 +08:00
const { isMobile } = deviceInfo;
2017-04-29 02:42:32 +08:00
2017-09-08 02:18:14 +08:00
const propTypes = {
intl: PropTypes.object.isRequired,
2017-09-08 02:18:14 +08:00
defaultFileName: PropTypes.string.isRequired,
handleSave: PropTypes.func.isRequired,
dispatchTogglePresentationDownloadable: PropTypes.func.isRequired,
2019-03-08 03:53:01 +08:00
fileValidMimeTypes: PropTypes.arrayOf(PropTypes.object).isRequired,
2017-09-08 02:18:14 +08:00
presentations: PropTypes.arrayOf(PropTypes.shape({
id: PropTypes.string.isRequired,
filename: PropTypes.string.isRequired,
isCurrent: PropTypes.bool.isRequired,
conversion: PropTypes.object,
upload: PropTypes.object,
})).isRequired,
2020-02-26 03:29:14 +08:00
isOpen: PropTypes.bool.isRequired,
2017-09-08 02:18:14 +08:00
};
const defaultProps = {
};
2017-04-29 02:42:32 +08:00
const intlMessages = defineMessages({
2017-12-05 00:10:37 +08:00
current: {
id: 'app.presentationUploder.currentBadge',
},
2017-04-29 02:42:32 +08:00
title: {
id: 'app.presentationUploder.title',
2017-11-02 00:13:18 +08:00
description: 'title of the modal',
2017-04-29 02:42:32 +08:00
},
message: {
id: 'app.presentationUploder.message',
2017-11-02 00:13:18 +08:00
description: 'message warning the types of files accepted',
2017-04-29 02:42:32 +08:00
},
uploadLabel: {
id: 'app.presentationUploder.uploadLabel',
description: 'confirm label when presentations are to be uploaded',
2017-04-29 02:42:32 +08:00
},
confirmLabel: {
id: 'app.presentationUploder.confirmLabel',
description: 'confirm label when no presentations are to be uploaded',
2017-04-29 02:42:32 +08:00
},
confirmDesc: {
id: 'app.presentationUploder.confirmDesc',
2017-11-02 00:13:18 +08:00
description: 'description of the confirm',
2017-04-29 02:42:32 +08:00
},
dismissLabel: {
id: 'app.presentationUploder.dismissLabel',
2017-11-02 00:13:18 +08:00
description: 'used in the button that close modal',
2017-04-29 02:42:32 +08:00
},
dismissDesc: {
id: 'app.presentationUploder.dismissDesc',
2017-11-02 00:13:18 +08:00
description: 'description of the dismiss',
2017-04-29 02:42:32 +08:00
},
dropzoneLabel: {
id: 'app.presentationUploder.dropzoneLabel',
2017-11-02 00:13:18 +08:00
description: 'message warning where drop files for upload',
2017-04-29 02:42:32 +08:00
},
dropzoneImagesLabel: {
id: 'app.presentationUploder.dropzoneImagesLabel',
description: 'message warning where drop images for upload',
},
2017-04-29 02:42:32 +08:00
browseFilesLabel: {
id: 'app.presentationUploder.browseFilesLabel',
2017-11-02 00:13:18 +08:00
description: 'message use on the file browser',
2017-04-29 02:42:32 +08:00
},
browseImagesLabel: {
id: 'app.presentationUploder.browseImagesLabel',
description: 'message use on the image browser',
},
2017-05-06 04:17:38 +08:00
fileToUpload: {
id: 'app.presentationUploder.fileToUpload',
2017-11-02 00:13:18 +08:00
description: 'message used in the file selected for upload',
2017-05-06 04:17:38 +08:00
},
rejectedError: {
id: 'app.presentationUploder.rejectedError',
description: 'some files rejected, please check the file mime types',
},
2017-09-08 02:18:14 +08:00
uploadProcess: {
id: 'app.presentationUploder.upload.progress',
2017-11-02 00:13:18 +08:00
description: 'message that indicates the percentage of the upload',
2017-09-08 02:18:14 +08:00
},
413: {
id: 'app.presentationUploder.upload.413',
2017-11-02 00:13:18 +08:00
description: 'error that file exceed the size limit',
2017-05-06 04:17:38 +08:00
},
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.',
},
2017-05-06 04:17:38 +08:00
conversionProcessingSlides: {
id: 'app.presentationUploder.conversion.conversionProcessingSlides',
2017-11-02 00:13:18 +08:00
description: 'indicates how many slides were converted',
2017-05-06 04:17:38 +08:00
},
2020-03-12 21:56:12 +08:00
genericError: {
id: 'app.presentationUploder.genericError',
description: 'generic error while uploading/converting',
},
2017-05-06 04:17:38 +08:00
genericConversionStatus: {
id: 'app.presentationUploder.conversion.genericConversionStatus',
2017-11-02 00:13:18 +08:00
description: 'indicates that file is being converted',
2017-05-06 04:17:38 +08:00
},
TIMEOUT: {
id: 'app.presentationUploder.conversion.timeout',
},
2017-05-06 04:17:38 +08:00
GENERATING_THUMBNAIL: {
id: 'app.presentationUploder.conversion.generatingThumbnail',
description: 'indicatess that it is generating thumbnails',
2017-05-06 04:17:38 +08:00
},
2017-09-27 03:45:33 +08:00
GENERATING_SVGIMAGES: {
id: 'app.presentationUploder.conversion.generatingSvg',
2017-11-02 00:13:18 +08:00
description: 'warns that it is generating svg images',
2017-09-27 03:45:33 +08:00
},
2017-05-06 04:17:38 +08:00
GENERATED_SLIDE: {
id: 'app.presentationUploder.conversion.generatedSlides',
2017-11-02 00:13:18 +08:00
description: 'warns that were slides generated',
2017-05-06 04:17:38 +08:00
},
2017-11-28 20:26:00 +08:00
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',
},
2021-04-28 03:45:14 +08:00
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',
},
2019-02-21 06:44:44 +08:00
isDownloadable: {
id: 'app.presentationUploder.isDownloadableLabel',
description: 'presentation is available for downloading by all viewers',
2019-02-21 06:44:44 +08:00
},
isNotDownloadable: {
id: 'app.presentationUploder.isNotDownloadableLabel',
description: 'presentation is not available for downloading the viewers',
2019-02-21 06:44:44 +08:00
},
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',
},
2019-05-15 00:51:13 +08:00
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',
},
2017-04-29 02:42:32 +08:00
});
2017-05-06 04:17:38 +08:00
class PresentationUploader extends Component {
2017-04-29 02:42:32 +08:00
constructor(props) {
super(props);
this.state = {
2020-02-26 03:29:14 +08:00
presentations: [],
2017-05-06 04:17:38 +08:00
disableActions: false,
toUploadCount: 0,
2017-04-29 02:42:32 +08:00
};
2020-02-26 03:29:14 +08:00
this.toastId = null;
2020-03-12 21:56:12 +08:00
this.hasError = null;
2020-02-26 03:29:14 +08:00
// handlers
2017-04-29 02:42:32 +08:00
this.handleFiledrop = this.handleFiledrop.bind(this);
this.handleConfirm = this.handleConfirm.bind(this);
this.handleDismiss = this.handleDismiss.bind(this);
this.handleRemove = this.handleRemove.bind(this);
2020-02-26 03:29:14 +08:00
this.handleCurrentChange = this.handleCurrentChange.bind(this);
this.handleDismissToast = this.handleDismissToast.bind(this);
this.handleToggleDownloadable = this.handleToggleDownloadable.bind(this);
// renders
this.renderDropzone = this.renderDropzone.bind(this);
this.renderPicDropzone = this.renderPicDropzone.bind(this);
this.renderPresentationList = this.renderPresentationList.bind(this);
this.renderPresentationItem = this.renderPresentationItem.bind(this);
this.renderPresentationItemStatus = this.renderPresentationItemStatus.bind(this);
this.renderToastList = this.renderToastList.bind(this);
this.renderToastItem = this.renderToastItem.bind(this);
// utilities
2017-09-27 03:45:33 +08:00
this.deepMergeUpdateFileKey = this.deepMergeUpdateFileKey.bind(this);
2020-02-26 03:29:14 +08:00
this.updateFileKey = this.updateFileKey.bind(this);
this.isDefault = this.isDefault.bind(this);
}
2020-02-26 03:29:14 +08:00
componentDidUpdate(prevProps) {
const { isOpen, presentations: propPresentations } = this.props;
2020-02-26 03:29:14 +08:00
const { presentations } = this.state;
2021-05-07 22:06:03 +08:00
//Updates presentation list when chat modal opens to avoid missing presentations
if (isOpen && !prevProps.isOpen) {
2021-05-07 21:52:12 +08:00
this.setState({
2021-05-13 02:43:45 +08:00
presentations: Object.values({
2021-05-07 21:52:12 +08:00
...propPresentations,
...presentations,
2021-05-13 02:43:45 +08:00
}),
2021-05-07 21:52:12 +08:00
});
}
// cleared local presetation state errors and set to presentations available on the server
2020-03-10 22:25:27 +08:00
if (presentations.length === 0 && propPresentations.length > 1) {
return this.setState({ presentations: propPresentations });
}
// Only presentation available is the default coming from the server.
// set as selectedToBeNextCurrentOnConfirm once upload / coversion complete
2020-03-10 22:25:27 +08:00
if (presentations.length === 0 && propPresentations.length === 1) {
if (propPresentations[0].upload.done && propPresentations[0].conversion.done) {
return this.setState({
presentations: propPresentations,
}, Session.set('selectedToBeNextCurrent', propPresentations[0].id));
}
2020-03-10 22:25:27 +08:00
}
2017-04-29 02:42:32 +08:00
if (presentations.length > 0) {
2021-05-07 22:06:03 +08:00
const selected = propPresentations.filter((p) => p.isCurrent);
if (selected.length > 0) Session.set('selectedToBeNextCurrent', selected[0].id);
2020-02-26 03:29:14 +08:00
}
2017-09-27 03:45:33 +08:00
2020-02-26 03:29:14 +08:00
if (this.toastId) {
if (!prevProps.isOpen && isOpen) {
this.handleDismissToast(this.toastId);
}
2017-09-27 03:45:33 +08:00
2020-02-26 03:29:14 +08:00
toast.update(this.toastId, {
render: this.renderToastList(),
});
}
2017-09-27 03:45:33 +08:00
}
componentWillUnmount() {
Session.set('showUploadPresentationView', false);
}
isDefault(presentation) {
const { defaultFileName } = this.props;
return presentation.filename === defaultFileName
&& !presentation.id.includes(defaultFileName);
}
2020-02-26 03:29:14 +08:00
handleDismissToast() {
return toast.dismiss(this.toastId);
2017-04-29 02:42:32 +08:00
}
handleFiledrop(files, files2) {
const { fileValidMimeTypes, intl } = this.props;
const { toUploadCount } = this.state;
2021-04-28 03:45:14 +08:00
const validMimes = fileValidMimeTypes.map((fileValid) => fileValid.mime);
const validExtentions = fileValidMimeTypes.map((fileValid) => fileValid.extension);
const [accepted, rejected] = _.partition(files
2021-04-28 03:45:14 +08:00
.concat(files2), (f) => (
validMimes.includes(f.type) || validExtentions.includes(`.${f.name.split('.').pop()}`)
));
const presentationsToUpload = accepted.map((file) => {
2017-11-22 22:47:27 +08:00
const id = _.uniqueId(file.name);
return {
file,
isDownloadable: false, // by default new presentations are set not to be downloadable
2017-11-22 22:47:27 +08:00
id,
filename: file.name,
isCurrent: false,
conversion: { done: false, error: false },
upload: { done: false, error: false, progress: 0 },
onProgress: (event) => {
if (!event.lengthComputable) {
this.deepMergeUpdateFileKey(id, 'upload', {
progress: 100,
done: true,
});
return;
}
this.deepMergeUpdateFileKey(id, 'upload', {
progress: (event.loaded / event.total) * 100,
done: event.loaded === event.total,
2017-09-23 04:45:31 +08:00
});
2017-11-22 22:47:27 +08:00
},
onConversion: (conversion) => {
this.deepMergeUpdateFileKey(id, 'conversion', conversion);
},
onUpload: (upload) => {
this.deepMergeUpdateFileKey(id, 'upload', upload);
},
onDone: (newId) => {
this.updateFileKey(id, 'id', newId);
2017-11-22 22:47:27 +08:00
},
};
});
2017-04-29 02:42:32 +08:00
this.setState(({ presentations }) => ({
2017-09-08 02:18:14 +08:00
presentations: presentations.concat(presentationsToUpload),
toUploadCount: (toUploadCount + presentationsToUpload.length),
}), () => {
// after the state is set (files have been dropped),
// make the first of the new presentations current
if (presentationsToUpload && presentationsToUpload.length) {
this.handleCurrentChange(presentationsToUpload[0].id);
}
});
if (rejected.length > 0) {
notify(intl.formatMessage(intlMessages.rejectedError), 'error');
}
2017-04-29 02:42:32 +08:00
}
handleRemove(item, withErr = false) {
if (withErr) {
const { presentations } = this.props;
this.hasError = false;
return this.setState({
presentations,
disableActions: false,
});
}
2020-02-26 03:29:14 +08:00
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 currentIndex = updatedPresentations.findIndex((p) => p.isCurrent);
const actualCurrentIndex = updatedPresentations.findIndex((p) => p.id === oldCurrentId);
2020-02-26 03:29:14 +08:00
if (currentIndex === -1 && updatedPresentations.length > 0) {
const commands = {};
const newCurrentIndex = actualCurrentIndex === -1 ? 0 : actualCurrentIndex;
commands[newCurrentIndex] = {
$apply: (presentation) => {
const p = presentation;
p.isCurrent = true;
return p;
},
};
2020-02-26 03:29:14 +08:00
const updatedCurrent = update(updatedPresentations, commands);
this.setState({ presentations: updatedCurrent });
}
});
2020-02-26 03:29:14 +08:00
}
handleCurrentChange(id) {
const { presentations, disableActions } = this.state;
2020-02-26 03:29:14 +08:00
if (disableActions) return;
2020-02-26 03:29:14 +08:00
const currentIndex = presentations.findIndex((p) => p.isCurrent);
const newCurrentIndex = presentations.findIndex((p) => p.id === id);
2020-02-26 03:29:14 +08:00
const commands = {};
// we can end up without a current presentation
if (currentIndex !== -1) {
commands[currentIndex] = {
$apply: (presentation) => {
const p = presentation;
p.isCurrent = false;
return p;
},
};
}
commands[newCurrentIndex] = {
2020-02-26 03:29:14 +08:00
$apply: (presentation) => {
const p = presentation;
p.isCurrent = true;
2020-02-26 03:29:14 +08:00
return p;
},
};
const presentationsUpdated = update(presentations, commands);
this.setState({ presentations: presentationsUpdated });
2020-02-26 03:29:14 +08:00
}
deepMergeUpdateFileKey(id, key, value) {
const applyValue = (toUpdate) => update(toUpdate, { $merge: value });
this.updateFileKey(id, key, applyValue, '$apply');
}
2020-02-26 03:29:14 +08:00
handleConfirm(hasNewUpload) {
const {
handleSave, selectedToBeNextCurrent,
2020-02-26 03:29:14 +08:00
} = this.props;
const { disableActions, presentations } = this.state;
const presentationsToSave = presentations;
this.setState({ disableActions: true });
if (hasNewUpload) {
this.toastId = toast.info(this.renderToastList(), {
hideProgressBar: true,
autoClose: false,
newestOnTop: true,
closeOnClick: true,
2020-02-26 03:29:14 +08:00
onClose: () => {
this.toastId = null;
},
});
}
if (this.toastId) Session.set('UploadPresentationToastId', this.toastId);
2020-02-26 03:29:14 +08:00
if (!disableActions) {
Session.set('showUploadPresentationView', false);
2020-02-26 03:29:14 +08:00
return handleSave(presentationsToSave)
.then(() => {
const hasError = presentations.some((p) => p.upload.error || p.conversion.error);
2020-02-26 03:29:14 +08:00
if (!hasError) {
this.setState({
disableActions: false,
toUploadCount: 0,
2020-02-26 03:29:14 +08:00
});
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.isCurrent);
2020-02-26 03:29:14 +08:00
if (newCurrent.upload.error || newCurrent.conversion.error) {
this.handleCurrentChange(selectedToBeNextCurrent);
2020-02-26 03:29:14 +08:00
}
});
})
.catch((error) => {
logger.error({
logCode: 'presentationuploader_component_save_error',
extraInfo: { error },
}, 'Presentation uploader catch error on confirm');
});
}
Session.set('showUploadPresentationView', false);
2020-02-26 03:29:14 +08:00
return null;
}
handleDismiss() {
const { presentations } = this.state;
const { presentations: propPresentations } = this.props;
const ids = new Set(propPresentations.map((d) => d.ID));
const merged = [
...propPresentations,
...presentations.filter((d) => !ids.has(d.ID)),
];
this.setState(
{ presentations: merged },
Session.set('showUploadPresentationView', false),
);
2020-02-26 03:29:14 +08:00
}
handleToggleDownloadable(item) {
const { dispatchTogglePresentationDownloadable } = this.props;
const { presentations } = this.state;
2020-02-26 03:29:14 +08:00
const oldDownloadableState = item.isDownloadable;
2017-09-27 03:45:33 +08:00
const outOfDatePresentationIndex = presentations.findIndex((p) => p.id === item.id);
const commands = {};
commands[outOfDatePresentationIndex] = {
$apply: (presentation) => {
const p = presentation;
p.isDownloadable = !oldDownloadableState;
2017-04-29 02:42:32 +08:00
return p;
},
};
const presentationsUpdated = update(presentations, commands);
2017-04-29 02:42:32 +08:00
this.setState({
presentations: presentationsUpdated,
});
// If the presentation has not be uploaded yet, adjusting the state suffices
// otherwise set previously uploaded presentation to [not] be downloadable
if (item.upload.done) {
dispatchTogglePresentationDownloadable(item, !oldDownloadableState);
2020-02-26 03:29:14 +08:00
}
}
2017-09-27 03:45:33 +08:00
updateFileKey(id, key, value, operation = '$set') {
this.setState(({ presentations }) => {
const fileIndex = presentations.findIndex((f) => f.id === id);
return fileIndex === -1 ? false : {
presentations: update(presentations, {
[fileIndex]: {
$apply: (file) => update(file, {
[key]: {
[operation]: value,
},
}),
},
}),
};
});
}
renderToastItem(item) {
const isUploading = !item.upload.done && item.upload.progress > 0;
const isConverting = !item.conversion.done && item.upload.done;
const hasError = item.conversion.error || item.upload.error;
const isProcessing = (isUploading || isConverting) && !hasError;
const itemClassName = {
[styles.done]: !isProcessing && !hasError,
[styles.err]: hasError,
[styles.loading]: isProcessing,
};
const statusInfoStyle = {
[styles.textErr]: hasError,
[styles.textInfo]: !hasError,
};
let icon = isProcessing ? 'blank' : 'check';
if (hasError) icon = 'circle_close';
return (
<div
key={item.id}
className={styles.uploadRow}
onClick={() => {
if (hasError || isProcessing) Session.set('showUploadPresentationView', true);
}}
>
<div className={styles.fileLine}>
<span className={styles.fileIcon}>
<Icon iconName="file" />
</span>
<span className={styles.toastFileName}>
<span>{item.filename}</span>
</span>
<span className={styles.statusIcon}>
<Icon iconName={icon} className={cx(itemClassName)} />
</span>
</div>
<div className={styles.statusInfo}>
<span className={cx(statusInfoStyle)}>{this.renderPresentationItemStatus(item)}</span>
</div>
</div>
);
}
2017-04-29 02:42:32 +08:00
renderPresentationList() {
const { presentations } = this.state;
2019-05-15 00:51:13 +08:00
const { intl } = this.props;
2017-04-29 02:42:32 +08:00
const presentationsSorted = presentations
2021-04-07 22:37:49 +08:00
.sort((a, b) => a.uploadTimestamp - b.uploadTimestamp)
.sort((a, b) => a.filename.localeCompare(b.filename))
.sort((a, b) => b.upload.progress - a.upload.progress)
.sort((a, b) => b.conversion.done - a.conversion.done)
.sort((a, b) => {
const aUploadNotTriggeredYet = !a.upload.done && a.upload.progress === 0;
const bUploadNotTriggeredYet = !b.upload.done && b.upload.progress === 0;
return bUploadNotTriggeredYet - aUploadNotTriggeredYet;
});
2017-04-29 02:42:32 +08:00
return (
2017-05-04 00:36:16 +08:00
<div className={styles.fileList}>
<table className={styles.table}>
2019-05-15 03:45:12 +08:00
<thead>
<tr>
<th className={styles.visuallyHidden} colSpan={3}>
{intl.formatMessage(intlMessages.filename)}
</th>
2019-05-15 03:45:12 +08:00
<th className={styles.visuallyHidden}>{intl.formatMessage(intlMessages.status)}</th>
<th className={styles.visuallyHidden}>{intl.formatMessage(intlMessages.options)}</th>
</tr>
</thead>
2017-05-04 00:36:16 +08:00
<tbody>
2021-04-28 03:45:14 +08:00
{presentationsSorted.map((item) => this.renderPresentationItem(item))}
2017-05-04 00:36:16 +08:00
</tbody>
</table>
</div>
2017-04-29 02:42:32 +08:00
);
}
2020-02-26 03:29:14 +08:00
renderToastList() {
const { presentations, toUploadCount } = this.state;
if (toUploadCount === 0) {
return this.handleDismissToast(this.toastId);
2017-09-08 02:18:14 +08:00
}
const { intl } = this.props;
2020-02-26 03:29:14 +08:00
let converted = 0;
2017-09-08 02:18:14 +08:00
2020-02-26 03:29:14 +08:00
let presentationsSorted = presentations
2021-05-07 22:06:03 +08:00
.filter((p) => (p.upload.progress || p.conversion.status) && p.file)
2020-02-26 03:29:14 +08:00
.sort((a, b) => a.uploadTimestamp - b.uploadTimestamp)
.sort((a, b) => a.conversion.done - b.conversion.done);
presentationsSorted = presentationsSorted
.splice(0, toUploadCount)
2020-02-26 03:29:14 +08:00
.map((p) => {
if (p.conversion.done) converted += 1;
return p;
2017-09-08 02:18:14 +08:00
});
2017-05-06 04:17:38 +08:00
2020-02-26 03:29:14 +08:00
let toastHeading = '';
const itemLabel = presentationsSorted.length > 1
? intl.formatMessage(intlMessages.itemPlural)
: intl.formatMessage(intlMessages.item);
2017-09-08 02:18:14 +08:00
2020-02-26 03:29:14 +08:00
if (converted === 0) {
toastHeading = intl.formatMessage(intlMessages.uploading, {
0: presentationsSorted.length,
1: itemLabel,
});
2017-05-06 04:17:38 +08:00
}
2020-02-26 03:29:14 +08:00
if (converted > 0 && converted !== presentationsSorted.length) {
toastHeading = intl.formatMessage(intlMessages.uploadStatus, {
0: converted,
1: presentationsSorted.length,
});
}
2017-05-06 04:17:38 +08:00
2020-02-26 03:29:14 +08:00
if (converted === presentationsSorted.length) {
toastHeading = intl.formatMessage(intlMessages.completed, {
0: converted,
});
2017-05-06 04:17:38 +08:00
}
2020-02-26 03:29:14 +08:00
return (
<div className={styles.toastWrapper}>
<div className={styles.uploadToastHeader}>
<Icon className={styles.uploadIcon} iconName="upload" />
<span className={styles.uploadToastTitle}>{toastHeading}</span>
</div>
<div className={styles.innerToast}>
<div>
<div>
{presentationsSorted.map((item) => this.renderToastItem(item))}
2020-02-26 03:29:14 +08:00
</div>
</div>
</div>
</div>
);
2017-05-06 04:17:38 +08:00
}
2017-04-29 02:42:32 +08:00
renderPresentationItem(item) {
2020-02-26 03:29:14 +08:00
const { disableActions, hasError: stateError } = this.state;
const {
intl, selectedToBeNextCurrent,
2020-02-26 03:29:14 +08:00
} = this.props;
2017-05-04 00:36:16 +08:00
const isActualCurrent = selectedToBeNextCurrent ? item.id === selectedToBeNextCurrent : item.isCurrent;
2017-11-28 20:26:00 +08:00
const isUploading = !item.upload.done && item.upload.progress > 0;
const isConverting = !item.conversion.done && item.upload.done;
const hasError = item.conversion.error || item.upload.error;
const isProcessing = (isUploading || isConverting) && !hasError;
2020-02-26 03:29:14 +08:00
if (!stateError && hasError) {
2020-03-12 21:56:12 +08:00
this.hasError = true;
2020-02-26 03:29:14 +08:00
}
2017-11-28 20:26:00 +08:00
const itemClassName = {
[styles.tableItemNew]: item.id.indexOf(item.filename) !== -1,
[styles.tableItemUploading]: isUploading,
[styles.tableItemConverting]: isConverting,
[styles.tableItemError]: hasError,
[styles.tableItemAnimated]: isProcessing,
};
2017-04-29 02:42:32 +08:00
const hideRemove = this.isDefault(item);
const formattedDownloadableLabel = !item.isDownloadable
2019-02-21 06:44:44 +08:00
? intl.formatMessage(intlMessages.isDownloadable)
: intl.formatMessage(intlMessages.isNotDownloadable);
2017-05-04 04:51:17 +08:00
const formattedDownloadableAriaLabel = `${formattedDownloadableLabel} ${item.filename}`;
const isDownloadableStyle = item.isDownloadable
? cx(styles.itemAction, styles.itemActionRemove, styles.checked)
: cx(styles.itemAction, styles.itemActionRemove);
2020-02-26 03:29:14 +08:00
2017-04-29 02:42:32 +08:00
return (
<tr
2017-05-04 00:36:16 +08:00
key={item.id}
2017-04-29 02:42:32 +08:00
className={cx(itemClassName)}
>
<td className={styles.tableItemIcon}>
2017-11-22 22:47:27 +08:00
<Icon iconName="file" />
2017-04-29 02:42:32 +08:00
</td>
2017-12-05 00:10:37 +08:00
{
2018-12-06 01:42:31 +08:00
isActualCurrent
? (
<th className={styles.tableItemCurrent}>
<span className={styles.currentLabel}>
{intl.formatMessage(intlMessages.current)}
2018-12-06 01:42:31 +08:00
</span>
</th>
)
: null
2017-12-05 00:10:37 +08:00
}
<th className={styles.tableItemName} colSpan={!isActualCurrent ? 2 : 0}>
2017-04-29 02:42:32 +08:00
<span>{item.filename}</span>
</th>
2017-11-28 20:26:00 +08:00
<td className={styles.tableItemStatus} colSpan={hasError ? 2 : 0}>
2017-05-06 04:17:38 +08:00
{this.renderPresentationItemStatus(item)}
2017-04-29 02:42:32 +08:00
</td>
{hasError ? null : (
2020-02-26 03:29:14 +08:00
<td className={styles.tableItemActions}>
<Button
disabled={disableActions}
className={isDownloadableStyle}
label={formattedDownloadableLabel}
aria-label={formattedDownloadableAriaLabel}
hideLabel
size="sm"
icon={item.isDownloadable ? 'download' : 'download-off'}
onClick={() => this.handleToggleDownloadable(item)}
/>
<Checkbox
ariaLabel={`${intl.formatMessage(intlMessages.setAsCurrentPresentation)} ${item.filename}`}
checked={item.isCurrent}
className={styles.itemAction}
keyValue={item.id}
2020-02-26 03:29:14 +08:00
onChange={() => this.handleCurrentChange(item.id)}
disabled={disableActions}
/>
{hideRemove ? null : (
<Button
2017-11-28 20:26:00 +08:00
disabled={disableActions}
className={cx(styles.itemAction, styles.itemActionRemove)}
2019-02-21 06:44:44 +08:00
label={intl.formatMessage(intlMessages.removePresentation)}
aria-label={`${intl.formatMessage(intlMessages.removePresentation)} ${item.filename}`}
size="sm"
icon="delete"
hideLabel
2017-11-28 20:26:00 +08:00
onClick={() => this.handleRemove(item)}
/>
2017-11-28 20:26:00 +08:00
)}
</td>
)}
2017-04-29 02:42:32 +08:00
</tr>
);
}
2020-02-26 03:29:14 +08:00
renderDropzone() {
const {
intl,
2020-02-26 03:29:14 +08:00
fileValidMimeTypes,
} = this.props;
const { disableActions } = this.state;
2020-03-12 21:56:12 +08:00
if (disableActions && !this.hasError) return null;
2020-03-12 21:56:12 +08:00
return this.hasError ? (
2020-02-26 03:29:14 +08:00
<div>
<Button
color="danger"
onClick={() => this.handleRemove(null, true)}
label={intl.formatMessage(intlMessages.clearErrors)}
aria-describedby="clearErrorDesc"
/>
<div id="clearErrorDesc" style={{ display: 'none' }}>
{intl.formatMessage(intlMessages.clearErrorsDesc)}
</div>
</div>
) : (
// 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.
<Dropzone
multiple
className={styles.dropzone}
activeClassName={styles.dropzoneActive}
accept={fileValidMimeTypes.map((fileValid) => fileValid.extension)}
2020-02-26 03:29:14 +08:00
disablepreview="true"
onDrop={this.handleFiledrop}
>
<Icon className={styles.dropzoneIcon} iconName="upload" />
<p className={styles.dropzoneMessage}>
2020-02-26 03:29:14 +08:00
{intl.formatMessage(intlMessages.dropzoneLabel)}
&nbsp;
<span className={styles.dropzoneLink}>
2020-02-26 03:29:14 +08:00
{intl.formatMessage(intlMessages.browseFilesLabel)}
</span>
</p>
</Dropzone>
);
}
2020-02-26 03:29:14 +08:00
renderPicDropzone() {
2017-05-04 00:36:16 +08:00
const {
intl,
} = this.props;
2017-04-29 02:42:32 +08:00
2017-05-06 04:17:38 +08:00
const { disableActions } = this.state;
2020-03-12 21:56:12 +08:00
if (disableActions && !this.hasError) return null;
2017-05-04 04:51:17 +08:00
2020-03-12 21:56:12 +08:00
return this.hasError ? (
2020-02-26 03:29:14 +08:00
<div>
<Button
color="danger"
onClick={() => this.handleRemove(null, true)}
label={intl.formatMessage(intlMessages.clearErrors)}
aria-describedby="clearErrorDesc"
/>
<div id="clearErrorDesc" style={{ display: 'none' }}>
{intl.formatMessage(intlMessages.clearErrorsDesc)}
</div>
</div>
) : (
2017-04-29 02:42:32 +08:00
<Dropzone
2017-09-08 02:18:14 +08:00
multiple
2017-04-29 02:42:32 +08:00
className={styles.dropzone}
activeClassName={styles.dropzoneActive}
2020-02-26 03:29:14 +08:00
rejectClassName={styles.dropzoneReject}
accept="image/*"
2018-12-06 01:42:31 +08:00
disablepreview="true"
2020-07-18 03:13:47 +08:00
data-test="fileUploadDropZone"
2017-04-29 02:42:32 +08:00
onDrop={this.handleFiledrop}
>
2020-07-18 03:13:47 +08:00
<Icon className={styles.dropzoneIcon} iconName="upload" />
2017-04-29 02:42:32 +08:00
<p className={styles.dropzoneMessage}>
2020-02-26 03:29:14 +08:00
{intl.formatMessage(intlMessages.dropzoneImagesLabel)}
&nbsp;
2017-04-29 02:42:32 +08:00
<span className={styles.dropzoneLink}>
2020-02-26 03:29:14 +08:00
{intl.formatMessage(intlMessages.browseImagesLabel)}
2017-04-29 02:42:32 +08:00
</span>
</p>
</Dropzone>
);
}
2020-02-26 03:29:14 +08:00
renderPresentationItemStatus(item) {
const { intl } = this.props;
2020-02-26 03:29:14 +08:00
if (!item.upload.done && item.upload.progress === 0) {
return intl.formatMessage(intlMessages.fileToUpload);
}
if (!item.upload.done && !item.upload.error) {
return intl.formatMessage(intlMessages.uploadProcess, {
0: Math.floor(item.upload.progress).toString(),
});
}
const constraint = {};
2020-02-26 03:29:14 +08:00
if (item.upload.done && item.upload.error) {
if (item.conversion.status === 'FILE_TOO_LARGE') {
constraint['0'] = ((item.conversion.maxFileSize) / 1000 / 1000).toFixed(2);
}
2020-03-12 21:56:12 +08:00
const errorMessage = intlMessages[item.upload.status] || intlMessages.genericError;
return intl.formatMessage(errorMessage, constraint);
2020-02-26 03:29:14 +08:00
}
if (!item.conversion.done && item.conversion.error) {
2020-03-12 21:56:12 +08:00
const errorMessage = intlMessages[item.conversion.status] || intlMessages.genericConversionStatus;
switch (item.conversion.status) {
case 'PAGE_COUNT_EXCEEDED':
constraint['0'] = item.conversion.maxNumberPages;
break;
case 'PDF_HAS_BIG_PAGE':
constraint['0'] = (item.conversion.bigPageSize / 1000 / 1000).toFixed(2);
break;
default:
break;
}
return intl.formatMessage(errorMessage, constraint);
2020-02-26 03:29:14 +08:00
}
if (!item.conversion.done && !item.conversion.error) {
if (item.conversion.pagesCompleted < item.conversion.numPages) {
return intl.formatMessage(intlMessages.conversionProcessingSlides, {
0: item.conversion.pagesCompleted,
1: item.conversion.numPages,
});
}
const conversionStatusMessage = intlMessages[item.conversion.status]
|| intlMessages.genericConversionStatus;
return intl.formatMessage(conversionStatusMessage);
}
return null;
}
render() {
const {
isOpen, isPresenter, intl,
2020-02-26 03:29:14 +08:00
} = this.props;
if (!isPresenter) return null;
2020-02-26 03:29:14 +08:00
const { presentations, disableActions } = this.state;
let hasNewUpload = false;
2020-02-26 03:29:14 +08:00
presentations.map((item) => {
if (item.id.indexOf(item.filename) !== -1 && item.upload.progress === 0) hasNewUpload = true;
});
2020-02-26 03:29:14 +08:00
return isOpen ? (
<div className={styles.modal}>
<div
className={styles.modalInner}
>
<div className={styles.modalHeader}>
<h1>{intl.formatMessage(intlMessages.title)}</h1>
2020-02-26 03:29:14 +08:00
<div className={styles.actionWrapper}>
<Button
className={styles.dismiss}
color="default"
onClick={this.handleDismiss}
2020-02-26 03:29:14 +08:00
label={intl.formatMessage(intlMessages.dismissLabel)}
aria-describedby={intl.formatMessage(intlMessages.dismissDesc)}
/>
<Button
className={styles.confirm}
color="primary"
onClick={() => this.handleConfirm(hasNewUpload)}
disabled={disableActions}
label={hasNewUpload
? intl.formatMessage(intlMessages.uploadLabel)
2021-05-07 22:06:03 +08:00
: intl.formatMessage(intlMessages.confirmLabel)}
2020-02-26 03:29:14 +08:00
/>
</div>
</div>
2020-02-26 03:29:14 +08:00
<div className={styles.modalHint}>
{`${intl.formatMessage(intlMessages.message)}`}
</div>
{this.renderPresentationList()}
2021-04-01 01:13:36 +08:00
{isMobile ? this.renderPicDropzone() : null}
{this.renderDropzone()}
</div>
2020-02-26 03:29:14 +08:00
</div>
) : null;
}
}
2017-04-29 02:42:32 +08:00
2017-09-23 04:45:31 +08:00
PresentationUploader.propTypes = propTypes;
PresentationUploader.defaultProps = defaultProps;
export default injectIntl(PresentationUploader);