554 lines
14 KiB
JavaScript
Raw Normal View History

import React, { useEffect, useState, useReducer, useRef } from 'react';
import axios from 'axios';
import PropTypes from 'prop-types';
import { isEqual, isEmpty, get } from 'lodash';
import { Modal, ModalFooter, PopUpWarning, useGlobalContext, 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(file.fileURL, {
headers: new Headers({ Origin: window.location.origin, mode: 'cors' }),
responseType: 'blob',
cancelToken: source.token,
// Should we add a timeout?
})
.then(({ data }) => {
const createdFile = new File([data], file.fileURL, {
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.log(err);
}
};
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, originalIndex, abortController }) => {
const formData = new FormData();
const headers = {};
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) {
const errorMessage = get(
err,
['response', 'payload', 'message', '0', 'messages', '0', 'message'],
get(err, ['response', 'payload', 'message'], null)
);
if (errorMessage) {
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);
return (
<>
<Modal isOpen={isOpen} onToggle={handleToggle} onClosed={handleClose}>
{/* header title */}
<ModalHeader
goBack={goBack}
headerBreadcrumbs={headerBreadcrumbs}
withBackButton={withBackButton}
/>
{/* body of the modal */}
{Component && (
<Component
onAbortUpload={handleAbortUpload}
addFilesToUpload={addFilesToUpload}
fileToEdit={fileToEdit}
filesToDownload={filesToDownload}
filesToUpload={filesToUpload}
formErrors={formErrors}
components={components}
isEditingUploadedFile={currentStep === 'edit'}
isFormDisabled={isFormDisabled}
onChange={handleChange}
onClickCancelUpload={handleCancelFileToUpload}
onClickDeleteFileToUpload={
currentStep === 'edit' ? handleClickDeleteFile : handleClickDeleteFileToUpload
}
onClickEditNewFile={handleGoToEditNewFile}
onGoToAddBrowseFiles={handleGoToAddBrowseFiles}
onSubmitEdit={
currentStep === 'edit' ? handleSubmitEditExistingFile : handleSubmitEditNewFile
}
onToggle={handleToggle}
toggleDisableForm={setIsFormDisabled}
ref={currentStep === 'edit' ? editModalRef : null}
setCropResult={handleSetCropResult}
setShouldDisplayNextButton={setDisplayNextButton}
withBackButton={withBackButton}
/>
)}
<ModalFooter>
<section>
<Button type="button" color="cancel" onClick={handleToggle}>
{formatMessage({ id: 'app.components.Button.cancel' })}
</Button>
{shouldDisplayNextButton && (
<Button
type="button"
color="primary"
onClick={handleClickNextButton}
disabled={isEmpty(filesToDownload)}
>
{formatMessage({ id: getTrad('button.next') })}
</Button>
)}
{currentStep === 'upload' && (
<Button
type="button"
color="success"
onClick={handleUploadFiles}
disabled={isFinishButtonDisabled}
>
{formatMessage(
{
id: getTrad(
`modal.upload-list.footer.button.${
filesToUploadLength > 1 ? 'plural' : 'singular'
}`
),
},
{ number: filesToUploadLength }
)}
</Button>
)}
{currentStep === 'edit-new' && (
<Button color="success" type="button" onClick={handleSubmitEditNewFile}>
{formatMessage({ id: 'form.button.finish' })}
</Button>
)}
{currentStep === 'edit' && (
<div style={{ margin: 'auto 0' }}>
<Button
disabled={isFormDisabled}
color="primary"
onClick={handleReplaceMedia}
style={{ marginRight: 10 }}
>
Replace media
</Button>
<Button
disabled={isFormDisabled}
color="success"
type="button"
onClick={handleSubmitEditExistingFile}
>
{formatMessage({ id: 'form.button.finish' })}
</Button>
</div>
)}
</section>
</ModalFooter>
</Modal>
<PopUpWarning
onClosed={handleCloseModalWarning}
isOpen={isWarningDeleteOpen}
toggleModal={toggleModalWarning}
popUpWarningType="danger"
onConfirm={handleConfirmDeleteFile}
/>
</>
);
};
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;