448 lines
16 KiB
JavaScript
448 lines
16 KiB
JavaScript
import Presentations from '/imports/api/presentations';
|
|
import React, { useState } from 'react';
|
|
import { useTracker } from 'meteor/react-meteor-data';
|
|
import Icon from '/imports/ui/components/common/icon/component';
|
|
import { makeCall } from '/imports/ui/services/api';
|
|
import Styled from '/imports/ui/components/presentation/presentation-uploader/styles';
|
|
import { toast } from 'react-toastify';
|
|
import { defineMessages } from 'react-intl';
|
|
import _ from 'lodash';
|
|
import { UploadingPresentations } from '/imports/api/presentations';
|
|
|
|
const TIMEOUT_CLOSE_TOAST = 1; //second
|
|
|
|
const intlMessages = defineMessages({
|
|
|
|
item: {
|
|
id: 'app.presentationUploder.item',
|
|
description: 'single item label',
|
|
},
|
|
itemPlural: {
|
|
id: 'app.presentationUploder.itemPlural',
|
|
description: 'plural item label',
|
|
},
|
|
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',
|
|
},
|
|
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',
|
|
},
|
|
413: {
|
|
id: 'app.presentationUploder.upload.413',
|
|
description: 'error that file exceed the size limit',
|
|
},
|
|
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',
|
|
},
|
|
FILE_TOO_LARGE: {
|
|
id: 'app.presentationUploder.upload.413',
|
|
description: 'error that file exceed the size limit',
|
|
},
|
|
IVALID_MIME_TYPE: {
|
|
id: 'app.presentationUploder.conversion.invalidMimeType',
|
|
description: 'warns user that the file\'s mime type is not supported or it doesn\'t match the extension',
|
|
},
|
|
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',
|
|
},
|
|
204: {
|
|
id: 'app.presentationUploder.conversion.204',
|
|
description: 'error indicating that the file has no content to capture',
|
|
},
|
|
fileToUpload: {
|
|
id: 'app.presentationUploder.fileToUpload',
|
|
description: 'message used in the file selected for upload',
|
|
},
|
|
uploadProcess: {
|
|
id: 'app.presentationUploder.upload.progress',
|
|
description: 'message that indicates the percentage of the upload',
|
|
},
|
|
badConnectionError: {
|
|
id: 'app.presentationUploder.connectionClosedError',
|
|
description: 'message indicating that the connection was closed',
|
|
},
|
|
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',
|
|
},
|
|
});
|
|
|
|
function renderPresentationItemStatus(item, intl) {
|
|
if ((("progress" in item) && item.progress === 0) || (("upload" in item) && item.upload.progress === 0 && !item.upload.error)) {
|
|
return intl.formatMessage(intlMessages.fileToUpload);
|
|
}
|
|
|
|
if (("progress" in item) && item.progress < 100 && !("conversion" in item)) {
|
|
return intl.formatMessage(intlMessages.uploadProcess, {
|
|
0: Math.floor(item.progress).toString(),
|
|
});
|
|
}
|
|
|
|
const constraint = {};
|
|
|
|
if (("upload" in item) && (item.upload.done && item.upload.error)) {
|
|
if (item.conversion.status === 'FILE_TOO_LARGE' || item.upload.status !== 413) {
|
|
constraint['0'] = ((item.conversion.maxFileSize) / 1000 / 1000).toFixed(2);
|
|
} else {
|
|
if (item.progress < 100) {
|
|
const errorMessage = intlMessages.badConnectionError;
|
|
return intl.formatMessage(errorMessage);
|
|
}
|
|
}
|
|
|
|
const errorMessage = intlMessages[item.upload.status] || intlMessages.genericError;
|
|
return intl.formatMessage(errorMessage, constraint);
|
|
}
|
|
|
|
if (("conversion" in item) && (!item.conversion.done && item.conversion.error)) {
|
|
const errorMessage = intlMessages[item.conversion.status] || intlMessages.genericConversionStatus;
|
|
|
|
switch (item.conversion.status) {
|
|
case 'CONVERSION_TIMEOUT':
|
|
constraint['0'] = item.conversion.numberPageError;
|
|
constraint['1'] = item.conversion.maxNumberOfAttempts;
|
|
break;
|
|
case 'FILE_TOO_LARGE':
|
|
constraint['0'] = ((item.conversion.maxFileSize) / 1000 / 1000).toFixed(2);
|
|
break;
|
|
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;
|
|
case 'IVALID_MIME_TYPE':
|
|
constraint['0'] = item.conversion.fileExtension;
|
|
constraint['1'] = item.conversion.fileMime;
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
return intl.formatMessage(errorMessage, constraint);
|
|
}
|
|
|
|
if ((("conversion" in item) && (!item.conversion.done && !item.conversion.error)) || (("progress" in item) && item.progress == 100)) {
|
|
let conversionStatusMessage
|
|
if ("conversion" in item) {
|
|
if (item.conversion.pagesCompleted < item.conversion.numPages) {
|
|
return intl.formatMessage(intlMessages.conversionProcessingSlides, {
|
|
0: item.conversion.pagesCompleted,
|
|
1: item.conversion.numPages,
|
|
});
|
|
}
|
|
|
|
conversionStatusMessage = intlMessages[item.conversion.status]
|
|
|| intlMessages.genericConversionStatus;
|
|
} else {
|
|
conversionStatusMessage = intlMessages.genericConversionStatus;
|
|
}
|
|
return intl.formatMessage(conversionStatusMessage);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
function renderToastItem(item, intl) {
|
|
|
|
const isUploading = ("progress" in item) && item.progress <= 100;
|
|
const isConverting = ("conversion" in item) && !item.conversion.done;
|
|
const hasError = ((("conversion" in item) && item.conversion.error) || (("upload" in item) && item.upload.error));
|
|
const isProcessing = (isUploading || isConverting) && !hasError;
|
|
|
|
let icon = isProcessing ? 'blank' : 'check';
|
|
if (hasError) icon = 'circle_close';
|
|
|
|
return (
|
|
<Styled.UploadRow
|
|
key={item.id || item.temporaryPresentationId}
|
|
onClick={() => {
|
|
if (hasError || isProcessing) Session.set('showUploadPresentationView', true);
|
|
}}
|
|
>
|
|
<Styled.FileLine>
|
|
<span>
|
|
<Icon iconName="file" />
|
|
</span>
|
|
<Styled.ToastFileName>
|
|
<span>{item.filename || item.name}</span>
|
|
</Styled.ToastFileName>
|
|
<Styled.StatusIcon>
|
|
<Styled.ToastItemIcon
|
|
done={!isProcessing && !hasError}
|
|
error={hasError}
|
|
loading={ isProcessing }
|
|
iconName={icon}
|
|
/>
|
|
</Styled.StatusIcon>
|
|
</Styled.FileLine>
|
|
<Styled.StatusInfo>
|
|
<Styled.StatusInfoSpan data-test="presentationStatusInfo" styles={hasError ? 'error' : 'info'}>
|
|
{renderPresentationItemStatus(item, intl)}
|
|
</Styled.StatusInfoSpan>
|
|
</Styled.StatusInfo>
|
|
</Styled.UploadRow>
|
|
);
|
|
}
|
|
|
|
const renderToastList = (presentations, intl) => {
|
|
|
|
let converted = 0;
|
|
|
|
let presentationsSorted = presentations
|
|
.sort((a, b) => a.uploadTimestamp - b.uploadTimestamp)
|
|
.sort((a, b) => {
|
|
const presADone = a.conversion ? a.conversion.done : false;
|
|
const presBDone = b.conversion ? b.conversion.done : false;
|
|
|
|
return presADone - presBDone
|
|
});
|
|
|
|
presentationsSorted
|
|
.forEach((p) => {
|
|
const presDone = p.conversion ? p.conversion.done : false;
|
|
if (presDone) converted += 1;
|
|
return p;
|
|
});
|
|
|
|
let toastHeading = '';
|
|
const itemLabel = presentationsSorted.length > 1
|
|
? intl.formatMessage(intlMessages.itemPlural)
|
|
: intl.formatMessage(intlMessages.item);
|
|
|
|
if (converted === 0) {
|
|
toastHeading = intl.formatMessage(intlMessages.uploading, {
|
|
0: presentationsSorted.length,
|
|
1: itemLabel,
|
|
});
|
|
}
|
|
|
|
if (converted > 0 && converted !== presentationsSorted.length) {
|
|
toastHeading = intl.formatMessage(intlMessages.uploadStatus, {
|
|
0: converted,
|
|
1: presentationsSorted.length,
|
|
});
|
|
}
|
|
|
|
if (converted === presentationsSorted.length) {
|
|
toastHeading = intl.formatMessage(intlMessages.completed, {
|
|
0: converted,
|
|
});
|
|
}
|
|
|
|
return (
|
|
<Styled.ToastWrapper data-test="presentationUploadProgressToast" >
|
|
<Styled.UploadToastHeader>
|
|
<Styled.UploadIcon iconName="upload" />
|
|
<Styled.UploadToastTitle>{toastHeading}</Styled.UploadToastTitle>
|
|
</Styled.UploadToastHeader>
|
|
<Styled.InnerToast>
|
|
<div>
|
|
<div>
|
|
{presentationsSorted.map((item) => renderToastItem(item, intl))}
|
|
</div>
|
|
</div>
|
|
</Styled.InnerToast>
|
|
</Styled.ToastWrapper>
|
|
);
|
|
}
|
|
|
|
|
|
function handleDismissToast(toastId) {
|
|
return toast.dismiss(toastId);
|
|
}
|
|
|
|
|
|
let alreadyRenderedPresList = [];
|
|
|
|
let enteredConversion = {};
|
|
|
|
export const PresentationUploaderToast = ({ intl }) => {
|
|
|
|
useTracker(() => {
|
|
|
|
const presentationsRenderedFalseAndConversionFalse = Presentations.find({ $or: [{renderedInToast: false}, {"conversion.done": false}] }).fetch();
|
|
|
|
const convertingPresentations = presentationsRenderedFalseAndConversionFalse.filter(p => !p.renderedInToast );
|
|
|
|
let conversionInterrupted = false;
|
|
|
|
// removing ones with errors. If presentation has an error status - we don't want to have it pending as uploading
|
|
convertingPresentations.map(p => {
|
|
if ("conversion" in p && p.conversion.error){
|
|
UploadingPresentations.remove({$or: [{temporaryPresentationId: p.temporaryPresentationId }, {id: p.id}]});
|
|
conversionInterrupted = true;
|
|
}
|
|
});
|
|
|
|
let toRemoveFromUploadingPresentations = [];
|
|
|
|
UploadingPresentations.find().fetch().map(p => { // main goal of this mapping is to sort out what doesn't need to be displayed
|
|
if (
|
|
( "upload" in p && p.upload.done ) // if presentation is marked as done - it's potentially to be removed
|
|
&& !p.subscriptionId // at upload stage or already converted
|
|
) {
|
|
if(convertingPresentations[0]) { //there are presentations being converted
|
|
convertingPresentations.forEach(cp => {
|
|
if (cp.temporaryPresentationId == p.temporaryPresentationId) { // if this presentation is being converted we don't want it to be marked as still uploading
|
|
toRemoveFromUploadingPresentations.push({temporaryPresentationId: p.temporaryPresentationId, id: p.id});
|
|
}
|
|
});
|
|
} else if (!enteredConversion[p.temporaryPresentationId]) { // upload stage is done and pesentation is entering conversion stage
|
|
enteredConversion[p.temporaryPresentationId] = true; // we mark that it has entered conversion stage
|
|
} else {
|
|
// presentation doesn't normally enter conversion twice
|
|
// so we set timout to await 4s before removal in case there is an error with some presentation
|
|
setTimeout((conversionInterrupted, p) => {
|
|
|
|
// border case of presenter change during conversion
|
|
// so we clear up the toasts so that they aren't permanently (only 4s) pending when presenter status is returned
|
|
if (typeof conversionInterrupted == "undefined") {
|
|
UploadingPresentations.remove({});
|
|
} else if (
|
|
// border case of error during conversion.
|
|
// It causes all of the presentations tpo re-enter in here,
|
|
// so we remove them if there wasn't a conversion error
|
|
!conversionInterrupted
|
|
) UploadingPresentations.remove({$or: [{temporaryPresentationId: p.temporaryPresentationId }, {id: p.id}]});
|
|
}, 4000);
|
|
}
|
|
}
|
|
});
|
|
|
|
toRemoveFromUploadingPresentations.forEach(p => UploadingPresentations.remove({$or: [{temporaryPresentationId: p.temporaryPresentationId }, {id: p.id}]}));
|
|
|
|
const uploadingPresentations = UploadingPresentations.find().fetch();
|
|
|
|
let presentationsToConvert = convertingPresentations.concat(uploadingPresentations);
|
|
// Updating or populating the "state" presentation list
|
|
presentationsToConvert.map(p => {
|
|
return {
|
|
filename: p.name || p.filename,
|
|
temporaryPresentationId: p.temporaryPresentationId,
|
|
presentationId: p.id,
|
|
hasError: p.conversion?.error || p.upload?.error,
|
|
lastModifiedUploader: p.lastModifiedUploader,
|
|
}
|
|
}).forEach(p => {
|
|
const docIndexAlreadyInList = alreadyRenderedPresList.findIndex(pres => {
|
|
return (pres.temporaryPresentationId === p.temporaryPresentationId || pres.presentationId === p.presentationId
|
|
|| (pres.lastModifiedUploader !== undefined && !pres.lastModifiedUploader && pres.filename === p.filename))});
|
|
if (docIndexAlreadyInList === -1) {
|
|
alreadyRenderedPresList.push({
|
|
filename: p.filename,
|
|
temporaryPresentationId: p.temporaryPresentationId,
|
|
presentationId: p.presentationId,
|
|
rendered: false,
|
|
lastModifiedUploader: p.lastModifiedUploader,
|
|
hasError: p.hasError,
|
|
});
|
|
} else {
|
|
alreadyRenderedPresList[docIndexAlreadyInList].temporaryPresentationId = p.temporaryPresentationId;
|
|
alreadyRenderedPresList[docIndexAlreadyInList].presentationId = p.presentationId;
|
|
alreadyRenderedPresList[docIndexAlreadyInList].lastModifiedUploader = p.lastModifiedUploader;
|
|
alreadyRenderedPresList[docIndexAlreadyInList].hasError = p.hasError;
|
|
}
|
|
})
|
|
let activeToast = Session.get("presentationUploaderToastId");
|
|
const showToast = presentationsToConvert.length > 0;
|
|
|
|
if (showToast && !activeToast) {
|
|
activeToast = toast.info(() => renderToastList(presentationsToConvert, intl), {
|
|
hideProgressBar: true,
|
|
autoClose: false,
|
|
newestOnTop: true,
|
|
closeOnClick: true,
|
|
className: "presentationUploaderToast toastClass",
|
|
onClose: () => {
|
|
presentationsToConvert = [];
|
|
if (alreadyRenderedPresList.every((pres) => pres.rendered)) {
|
|
makeCall('setPresentationRenderedInToast').then(() => {
|
|
Session.set("presentationUploaderToastId", null);
|
|
});
|
|
alreadyRenderedPresList.length = 0;
|
|
}
|
|
},
|
|
});
|
|
Session.set("presentationUploaderToastId", activeToast);
|
|
} else if (!showToast && activeToast) {
|
|
handleDismissToast(activeToast);
|
|
Session.set("presentationUploaderToastId", null);
|
|
} else {
|
|
toast.update(activeToast, {
|
|
render: renderToastList(presentationsToConvert, intl),
|
|
});
|
|
}
|
|
|
|
let temporaryPresentationIdListToSetAsRendered = presentationsToConvert.filter(p =>
|
|
("conversion" in p && (p.conversion.done || p.conversion.error)))
|
|
|
|
temporaryPresentationIdListToSetAsRendered = temporaryPresentationIdListToSetAsRendered.map(p => {
|
|
index = alreadyRenderedPresList.findIndex(pres => (pres.temporaryPresentationId === p.temporaryPresentationId || pres.presentationId === p.id));
|
|
if (index !== -1) {
|
|
alreadyRenderedPresList[index].rendered = true;
|
|
}
|
|
return p.temporaryPresentationId
|
|
});
|
|
|
|
if (alreadyRenderedPresList.every((pres) => pres.rendered && !pres.hasError)) {
|
|
|
|
setTimeout(() => {
|
|
makeCall('setPresentationRenderedInToast');
|
|
alreadyRenderedPresList.length = 0;
|
|
}, TIMEOUT_CLOSE_TOAST * 1000);
|
|
}
|
|
|
|
}, []);
|
|
return null;
|
|
}
|
|
|
|
export default {
|
|
handleDismissToast,
|
|
renderPresentationItemStatus,
|
|
}
|