Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save ambrinchaudhary/7f824e1d71660b9816248f40618a8149 to your computer and use it in GitHub Desktop.

Select an option

Save ambrinchaudhary/7f824e1d71660b9816248f40618a8149 to your computer and use it in GitHub Desktop.
// /Users/ambrinchaudhary/Liferay/portal/modules/apps/item-selector/item-selector-taglib/src/main/resources/META-INF/resources/js/item_selector_uploader/MultipleFileUploader.js
// (This would be a new file)
import {ClayButtonWithIcon, ClayButton} from '@clayui/button';
import ClayForm from '@clayui/form';
import ClayLayout from '@clayui/layout';
import ClayProgressBar from '@clayui/progress-bar';
import {useIsMounted} from '@liferay/frontend-js-react-web';
import classNames from 'classnames';
import {formatStorage, sub} from 'frontend-js-web';
import PropTypes from 'prop-types';
import React, {useEffect, useMemo, useState, useCallback} from 'react';
import {ErrorCode, useDropzone} from 'react-dropzone';
import ItemSelectorPreview from '../item_selector_preview/ItemSelectorPreview';
import DragFileBackground from './components/DragFilePlaceholder';
import getPreviewProps from './utils/getPreviewProps';
import getUploadErrorMessage from './utils/getUploadErrorMessage';
import sendFile from './utils/sendFile';
// Helper to generate a unique ID for file entries
const generateFileId = (file) => `${file.name}-${file.lastModified}-${file.size}-${Math.random().toString(36).substr(2, 9)}`;
function MultipleFileUploader({
// closeCaption, // May need rethinking for multiple files
editImageURL,
itemSelectedEventName,
maxFileSize: initialMaxFileSizeString = Liferay.PropsValues
.UPLOAD_SERVLET_REQUEST_IMPL_MAX_SIZE,
mimeTypeRestriction,
uploadItemReturnType,
uploadItemURL,
validExtensions,
maxFiles = 0, // 0 for no limit, otherwise positive integer
}) {
const [filesInfo, setFilesInfo] = useState([]);
const [globalErrorMessage, setGlobalErrorMessage] = useState('');
const isMounted = useIsMounted();
const maxFileSize = Number(initialMaxFileSizeString);
const CLIENT_ERRORS = useMemo(
() => ({
[ErrorCode.FileInvalidType]: sub(
Liferay.Language.get(
'please-enter-a-file-with-a-valid-extension-x'
),
[validExtensions]
),
[ErrorCode.FileTooLarge]: sub(
Liferay.Language.get(
'please-enter-a-file-with-a-valid-file-size-no-larger-than-x'
),
[formatStorage(maxFileSize)]
),
// ErrorCode.TooManyFiles is handled by react-dropzone's maxFiles option
}),
[maxFileSize, validExtensions]
);
const updateFileState = useCallback((fileId, updates) => {
setFilesInfo((prevFilesInfo) =>
prevFilesInfo.map((info) =>
info.id === fileId ? {...info, ...updates} : info
)
);
}, []);
const {getInputProps, getRootProps, isDragActive} = useDropzone({
accept: validExtensions === '*' ? undefined : validExtensions,
maxSize: maxFileSize,
maxFiles: maxFiles,
multiple: true,
onDropAccepted: (acceptedFiles) => {
setGlobalErrorMessage('');
const newFilesToUpload = acceptedFiles.map((file) => ({
id: generateFileId(file),
file,
progress: null,
errorMessage: null,
serverData: null,
uploadClient: null,
status: 'pending',
}));
setFilesInfo((prevFilesInfo) => {
const currentIds = new Set(prevFilesInfo.map(f => f.id));
const uniqueNewFiles = newFilesToUpload.filter(nf => !currentIds.has(nf.id));
return [...prevFilesInfo, ...uniqueNewFiles];
});
},
onDropRejected: (fileRejections) => {
let message = Liferay.Language.get('some-files-were-rejected');
const firstRejection = fileRejections?.[0];
if (firstRejection) {
const firstErrorCode = firstRejection.errors?.[0]?.code;
if (firstErrorCode === ErrorCode.TooManyFiles) {
message = sub(Liferay.Language.get('please-select-no-more-than-x-files'), [maxFiles]);
}
else if (CLIENT_ERRORS[firstErrorCode]) {
message = CLIENT_ERRORS[firstErrorCode];
}
}
setGlobalErrorMessage(message);
},
});
useEffect(() => {
filesInfo.forEach((info) => {
if (info.status === 'pending' && info.file && !info.uploadClient) {
updateFileState(info.id, { status: 'uploading' });
const client = sendFile({
file: info.file,
fileFieldName: 'itemSelectorFileName', // Consider making this a prop
onError: (error) => {
if (!isMounted()) return;
// Check if already aborted
const latestInfo = filesInfo.find(fi => fi.id === info.id);
if (latestInfo && latestInfo.status === 'aborted') return;
updateFileState(info.id, {
errorMessage: getUploadErrorMessage(error),
status: 'error',
progress: null,
uploadClient: null,
});
},
onProgress: (progressValue) => {
if (!isMounted()) return;
const latestInfo = filesInfo.find(fi => fi.id === info.id);
if (latestInfo && latestInfo.status === 'aborted') return;
updateFileState(info.id, { progress: progressValue });
},
onSuccess: (itemData) => {
if (!isMounted()) return;
const latestInfo = filesInfo.find(fi => fi.id === info.id);
if (latestInfo && latestInfo.status === 'aborted') return;
if (itemData.success) {
updateFileState(info.id, {
serverData: itemData,
status: 'success',
progress: 100,
uploadClient: null,
});
if (itemSelectedEventName && uploadItemReturnType && Liferay.Events) {
Liferay.Events.emit(itemSelectedEventName, {
// Adjust data structure as needed for multiple items
data: itemData[uploadItemReturnType] || itemData,
fileInfo: {name: info.file.name, size: info.file.size}
});
}
} else {
updateFileState(info.id, {
errorMessage: getUploadErrorMessage(itemData.error, maxFileSize),
status: 'error',
progress: null,
uploadClient: null,
});
}
},
url: uploadItemURL,
});
// Store client to allow aborting
updateFileState(info.id, { uploadClient: client });
}
});
}, [filesInfo, isMounted, maxFileSize, itemSelectedEventName, uploadItemReturnType, uploadItemURL, updateFileState]);
const handleRemoveOrCancelFile = (fileIdToRemove) => {
setFilesInfo((prevFilesInfo) =>
prevFilesInfo.filter((info) => {
if (info.id === fileIdToRemove) {
if (info.uploadClient && info.status === 'uploading') {
info.uploadClient.abort();
}
return false; // Remove from list
}
return true;
})
);
};
const clearAll = () => {
filesInfo.forEach(info => {
if (info.uploadClient && info.status === 'uploading') {
info.uploadClient.abort();
}
});
setFilesInfo([]);
setGlobalErrorMessage('');
};
const hasUploadingFiles = filesInfo.some(f => f.status === 'uploading');
return (
<>
<div
{...getRootProps({
className: classNames('dropzone', {
'dropzone-drag-active': isDragActive,
'dropzone-uploading': hasUploadingFiles,
}),
})}
>
<input {...getInputProps()} disabled={hasUploadingFiles} />
<DragFileBackground mimeTypeRestriction={mimeTypeRestriction} />
</div>
{globalErrorMessage && (
<ClayForm.FeedbackGroup className="has-error mt-2">
<ClayForm.FeedbackItem>
<ClayForm.FeedbackIndicator symbol="exclamation-full" />
{globalErrorMessage}
</ClayForm.FeedbackItem>
</ClayForm.FeedbackGroup>
)}
{filesInfo.length > 0 && (
<div className="multi-file-upload-list mt-3">
{filesInfo.map((info) => (
<div key={info.id} className="file-item mb-3 p-2 border rounded">
<ClayLayout.Row align="center">
<ClayLayout.Col md={editImageURL && info.status === 'success' ? 6 : 10}>
<p className="text-truncate mb-1" title={info.file.name}>
<strong>{info.file.name}</strong> ({formatStorage(info.file.size)})
</p>
{info.status === 'uploading' && info.progress !== null && (
<ClayProgressBar value={info.progress} />
)}
{info.status === 'error' && info.errorMessage && (
<ClayForm.FeedbackGroup className="has-error">
<ClayForm.FeedbackItem>
<ClayForm.FeedbackIndicator symbol="exclamation-full" />
{info.errorMessage}
</ClayForm.FeedbackItem>
</ClayForm.FeedbackGroup>
)}
{info.status === 'success' && !editImageURL && (
<div className="text-success">{Liferay.Language.get('upload-successful')}</div>
)}
</ClayLayout.Col>
{editImageURL && info.status === 'success' && info.serverData && (
<ClayLayout.Col md={4}>
<div className="item-selector-preview-container-small"> {/* Needs styling */}
<ItemSelectorPreview
{...getPreviewProps({
file: info.file,
itemData: info.serverData,
itemSelectedEventName, // Event is emitted in useEffect
uploadItemReturnType,
})}
editImageURL={editImageURL} // May need dynamic URL if item-specific
// handleClose={() => { /* Optional: remove preview or item */ }}
/>
</div>
</ClayLayout.Col>
)}
<ClayLayout.Col md={2} className="text-right">
<ClayButtonWithIcon
aria-label={info.status === 'uploading' ? Liferay.Language.get('cancel') : Liferay.Language.get('remove')}
borderless
displayType="secondary"
onClick={() => handleRemoveOrCancelFile(info.id)}
symbol={info.status === 'uploading' ? 'times-circle' : 'times'}
/>
</ClayLayout.Col>
</ClayLayout.Row>
</div>
))}
<ClayButton displayType="secondary" onClick={clearAll} className="mt-2">
{Liferay.Language.get('clear-all')}
</ClayButton>
</div>
)}
</>
);
}
MultipleFileUploader.propTypes = {
// editImageURL: PropTypes.string, // URL or a function (itemData) => url
// itemSelectedEventName: PropTypes.string.isRequired,
// maxFileSize: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
// maxFiles: PropTypes.number,
// mimeTypeRestriction: PropTypes.string,
// uploadItemReturnType: PropTypes.string.isRequired,
// uploadItemURL: PropTypes.string.isRequired,
// validExtensions: PropTypes.string,
// ... (Copy relevant props from SingleFileUploader and add new ones like maxFiles)
};
// export default MultipleFileUploader;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment