import React, { useEffect, useState, useReducer, useRef } from 'react'; import axios from 'axios'; import PropTypes from 'prop-types'; import { isEqual, isEmpty, get, set } from 'lodash'; import { Modal, ModalFooter, PopUpWarning, useGlobalContext, auth, request, } from 'strapi-helper-plugin'; import { Button } from '@buffetjs/core'; import pluginId from '../../pluginId'; import { getFilesToDownload, getTrad, getYupError, urlSchema } from '../../utils'; import ModalHeader from '../../components/ModalHeader'; import stepper from './stepper'; import init from './init'; import reducer, { initialState } from './reducer'; const ModalStepper = ({ initialFileToEdit, initialStep, isOpen, onClosed, onDeleteMedia, onToggle, }) => { const { formatMessage } = useGlobalContext(); const [isWarningDeleteOpen, setIsWarningDeleteOpen] = useState(false); const [shouldDeleteFile, setShouldDeleteFile] = useState(false); const [isFormDisabled, setIsFormDisabled] = useState(false); const [formErrors, setFormErrors] = useState(null); const [shouldRefetch, setShouldRefetch] = useState(false); const [displayNextButton, setDisplayNextButton] = useState(false); const [reducerState, dispatch] = useReducer(reducer, initialState, init); const { currentStep, fileToEdit, filesToDownload, filesToUpload } = reducerState.toJS(); const { Component, components, headerBreadcrumbs, next, prev, withBackButton } = stepper[ currentStep ]; const filesToUploadLength = filesToUpload.length; const toggleRef = useRef(onToggle); const editModalRef = useRef(); const downloadFilesRef = useRef(); useEffect(() => { if (currentStep === 'upload') { // Close the modal if (filesToUploadLength === 0) { // Passing true to the onToggle prop will refetch the data when the modal closes toggleRef.current(true); } else { // Download files from url downloadFilesRef.current(); } } // eslint-disable-next-line react-hooks/exhaustive-deps }, [filesToUploadLength, currentStep]); useEffect(() => { if (isOpen) { goTo(initialStep); if (initialFileToEdit) { dispatch({ type: 'INIT_FILE_TO_EDIT', fileToEdit: initialFileToEdit, }); } } // Disabling the rule because we just want to let the ability to open the modal // at a specific step then we will let the stepper handle the navigation // eslint-disable-next-line react-hooks/exhaustive-deps }, [isOpen]); const addFilesToUpload = ({ target: { value } }) => { dispatch({ type: 'ADD_FILES_TO_UPLOAD', filesToUpload: value, }); goTo(next); }; downloadFilesRef.current = async () => { const files = getFilesToDownload(filesToUpload); try { await Promise.all( files.map(file => { const { source } = file; return axios .get(`${strapi.backendURL}/${pluginId}/proxy?url=${file.fileURL}`, { headers: { Authorization: `Bearer ${auth.getToken()}` }, responseType: 'blob', cancelToken: source.token, timeout: 60000, }) .then(({ data }) => { const fileName = file.fileInfo.name; const createdFile = new File([data], fileName, { type: data.type, }); dispatch({ type: 'FILE_DOWNLOADED', blob: createdFile, originalIndex: file.originalIndex, fileTempId: file.tempId, }); }) .catch(err => { console.error('fetch file error', err); dispatch({ type: 'SET_FILE_TO_DOWNLOAD_ERROR', originalIndex: file.originalIndex, fileTempId: file.tempId, }); }); }) ); } catch (err) { // Silent } }; const handleAbortUpload = () => { const { abortController } = fileToEdit; abortController.abort(); dispatch({ type: 'ON_ABORT_UPLOAD', }); }; const handleCancelFileToUpload = fileOriginalIndex => { const fileToCancel = filesToUpload.find(file => file.originalIndex === fileOriginalIndex); const { source } = fileToCancel; // Cancel if (source) { // Cancel dowload file upload with axios source.cancel('Operation canceled by the user.'); } else { // Cancel upload with fetch fileToCancel.abortController.abort(); } dispatch({ type: 'REMOVE_FILE_TO_UPLOAD', fileIndex: fileOriginalIndex, }); }; const handleChange = ({ target: { name, value } }) => { let val = value; let type = 'ON_CHANGE'; if (name === 'url') { setFormErrors(null); val = value.split('\n'); type = 'ON_CHANGE_URLS_TO_DOWNLOAD'; } dispatch({ type, keys: name, value: val, }); }; const handleConfirmDeleteFile = () => { setShouldDeleteFile(true); toggleModalWarning(); }; const handleClickNextButton = async () => { try { await urlSchema.validate( { filesToDownload: filesToDownload.filter(url => !isEmpty(url)) }, { abortEarly: false } ); setFormErrors(null); // Navigate to next step dispatch({ type: 'ADD_URLS_TO_FILES_TO_UPLOAD', nextStep: next, }); } catch (err) { const formattedErrors = getYupError(err); setFormErrors(formattedErrors.filesToDownload); } }; const handleClickDeleteFile = async () => { toggleModalWarning(); }; const handleClickDeleteFileToUpload = fileIndex => { dispatch({ type: 'REMOVE_FILE_TO_UPLOAD', fileIndex, }); if (currentStep === 'edit-new') { dispatch({ type: 'RESET_FILE_TO_EDIT', }); goNext(); } }; const handleClose = () => { onClosed(); setIsFormDisabled(false); setDisplayNextButton(false); setFormErrors(null); setShouldRefetch(false); dispatch({ type: 'RESET_PROPS', }); }; const handleCloseModalWarning = async () => { if (shouldDeleteFile) { const { id } = fileToEdit; onDeleteMedia(id); } }; const handleGoToEditNewFile = fileIndex => { dispatch({ type: 'SET_FILE_TO_EDIT', fileIndex, }); goTo('edit-new'); }; const handleGoToAddBrowseFiles = () => { dispatch({ type: 'CLEAN_FILES_ERROR', }); goBack(); }; const handleSetCropResult = blob => { dispatch({ type: 'SET_CROP_RESULT', blob, }); }; const handleSubmitEditNewFile = e => { e.preventDefault(); dispatch({ type: 'ON_SUBMIT_EDIT_NEW_FILE', }); goNext(); }; const handleSubmitEditExistingFile = async ( e, shouldDuplicateMedia = false, file = fileToEdit.file ) => { e.preventDefault(); dispatch({ type: 'ON_SUBMIT_EDIT_EXISTING_FILE', }); const headers = {}; const formData = new FormData(); // If the file has been cropped we need to add it to the formData // otherwise we just don't send it const didCropFile = file instanceof File; const { abortController, id, fileInfo } = fileToEdit; const requestURL = shouldDuplicateMedia ? `/${pluginId}` : `/${pluginId}?id=${id}`; if (didCropFile) { formData.append('files', file); } formData.append('fileInfo', JSON.stringify(fileInfo)); try { await request( requestURL, { method: 'POST', headers, body: formData, signal: abortController.signal, }, false, false ); // Close the modal and refetch data toggleRef.current(true); } catch (err) { console.error(err); const status = get(err, 'response.status', get(err, 'status', null)); const statusText = get(err, 'response.statusText', get(err, 'statusText', null)); const errorMessage = get( err, ['response', 'payload', 'message', '0', 'messages', '0', 'message'], get(err, ['response', 'payload', 'message'], statusText) ); if (status) { dispatch({ type: 'SET_FILE_TO_EDIT_ERROR', errorMessage, }); } } }; const handleReplaceMedia = () => { editModalRef.current.click(); }; const handleToggle = () => { if (filesToUploadLength > 0) { // eslint-disable-next-line no-alert const confirm = window.confirm( formatMessage({ id: getTrad('window.confirm.close-modal.files') }) ); if (!confirm) { return; } } if (!isEqual(initialFileToEdit, fileToEdit) && currentStep === 'edit') { // eslint-disable-next-line no-alert const confirm = window.confirm( formatMessage({ id: getTrad('window.confirm.close-modal.file') }) ); if (!confirm) { return; } } onToggle(shouldRefetch); }; const handleUploadFiles = async () => { dispatch({ type: 'SET_FILES_UPLOADING_STATE', }); const requests = filesToUpload.map( async ({ file, fileInfo, originalName, originalIndex, abortController }) => { const formData = new FormData(); const headers = {}; if (originalName === fileInfo.name) { set(fileInfo, 'name', null); } formData.append('files', file); formData.append('fileInfo', JSON.stringify(fileInfo)); try { await request( `/${pluginId}`, { method: 'POST', headers, body: formData, signal: abortController.signal, }, false, false ); setShouldRefetch(true); dispatch({ type: 'REMOVE_FILE_TO_UPLOAD', fileIndex: originalIndex, }); } catch (err) { console.error(err); const status = get(err, 'response.status', get(err, 'status', null)); const statusText = get(err, 'response.statusText', get(err, 'statusText', null)); const errorMessage = get( err, ['response', 'payload', 'message', '0', 'messages', '0', 'message'], get(err, ['response', 'payload', 'message'], statusText) ); if (status) { dispatch({ type: 'SET_FILE_ERROR', fileIndex: originalIndex, errorMessage, }); } } } ); await Promise.all(requests); }; const goBack = () => { goTo(prev); }; const goNext = () => { if (next === null) { onToggle(); return; } goTo(next); }; const goTo = to => { dispatch({ type: 'GO_TO', to, }); }; const toggleModalWarning = () => { setIsWarningDeleteOpen(prev => !prev); }; const shouldDisplayNextButton = currentStep === 'browse' && displayNextButton; const isFinishButtonDisabled = filesToUpload.some(file => file.isDownloading || file.isUploading); const areButtonsDisabledOnEditExistingFile = currentStep === 'edit' && fileToEdit.isUploading === true; return ( <> {/* header title */} {/* body of the modal */} {Component && ( )}
{shouldDisplayNextButton && ( )} {currentStep === 'upload' && ( )} {currentStep === 'edit-new' && ( )} {currentStep === 'edit' && (
)}
); }; ModalStepper.defaultProps = { initialFileToEdit: null, initialStep: 'browse', onClosed: () => {}, onDeleteMedia: () => {}, onToggle: () => {}, }; ModalStepper.propTypes = { initialFileToEdit: PropTypes.object, initialStep: PropTypes.string, isOpen: PropTypes.bool.isRequired, onClosed: PropTypes.func, onDeleteMedia: PropTypes.func, onToggle: PropTypes.func, }; export default ModalStepper;