Merge pull request #5664 from strapi/features/media-lib-upload-url

ML/ add url upload
This commit is contained in:
cyril lopez 2020-04-01 11:13:33 +02:00 committed by GitHub
commit 2830cc817e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 1550 additions and 72 deletions

View File

@ -28,6 +28,7 @@ const Card = ({
type, type,
url, url,
withFileCaching, withFileCaching,
withoutFileInfo,
}) => { }) => {
const fileSize = formatBytes(size, 0); const fileSize = formatBytes(size, 0);
const fileType = mime || type; const fileType = mime || type;
@ -50,14 +51,22 @@ const Card = ({
<Border color={hasError ? 'orange' : 'mediumBlue'} shown={checked || hasError} /> <Border color={hasError ? 'orange' : 'mediumBlue'} shown={checked || hasError} />
{children} {children}
</CardImgWrapper> </CardImgWrapper>
{!withoutFileInfo ? (
<>
<Flex> <Flex>
<Title>{name}</Title> <Title>{name}</Title>
<Tag label={getType(fileType)} /> <Tag label={getType(fileType)} />
</Flex> </Flex>
<Text color="grey" fontSize="xs" ellipsis> <Text color="grey" fontSize="xs" ellipsis>
{`${getExtension(fileType)} - ${fileSize}`} {!withoutFileInfo && `${getExtension(fileType)} - ${fileSize}`}
</Text> </Text>
{hasError && <ErrorMessage>{errorMessage}</ErrorMessage>} </>
) : (
<Text lineHeight="13px" />
)}
{hasError && <ErrorMessage title={errorMessage}>{errorMessage}</ErrorMessage>}
</Wrapper> </Wrapper>
); );
}; };
@ -78,6 +87,7 @@ Card.defaultProps = {
type: null, type: null,
url: null, url: null,
withFileCaching: true, withFileCaching: true,
withoutFileInfo: false,
}; };
Card.propTypes = { Card.propTypes = {
@ -96,6 +106,7 @@ Card.propTypes = {
type: PropTypes.string, type: PropTypes.string,
url: PropTypes.string, url: PropTypes.string,
withFileCaching: PropTypes.bool, withFileCaching: PropTypes.bool,
withoutFileInfo: PropTypes.bool,
}; };
export default memo(Card); export default memo(Card);

View File

@ -0,0 +1,13 @@
import styled from 'styled-components';
import ContainerFluid from '../ContainerFluid';
const Wrapper = styled(ContainerFluid)`
margin-bottom: 0.3rem;
padding-top: 2.3rem;
textarea {
height: 10.1rem;
}
`;
export default Wrapper;

View File

@ -0,0 +1,42 @@
import React from 'react';
import { Inputs } from '@buffetjs/custom';
import { useGlobalContext } from 'strapi-helper-plugin';
import PropTypes from 'prop-types';
import Wrapper from './Wrapper';
import { getTrad } from '../../utils';
const InputUploadURL = ({ onChange, value }) => {
const { formatMessage } = useGlobalContext();
const label = formatMessage({ id: getTrad('input.url.label') });
const description = formatMessage({ id: getTrad('input.url.description') });
return (
<Wrapper>
<div className="row">
<div className="col-12">
<Inputs
autoFocus
type="textarea"
name="url"
onChange={onChange}
label={label}
description={description}
value={value.join('\n')}
/>
</div>
</div>
</Wrapper>
);
};
InputUploadURL.defaultProps = {
onChange: () => {},
value: [],
};
InputUploadURL.propTypes = {
onChange: PropTypes.func,
value: PropTypes.arrayOf(PropTypes.string),
};
export default InputUploadURL;

View File

@ -13,6 +13,10 @@ const ModalNavWrapper = ({ children, links, renderRightContent, initialTab }) =>
const handleGoTo = link => { const handleGoTo = link => {
setTo(link.to); setTo(link.to);
if (link.onClick) {
link.onClick(link.to);
}
}; };
return ( return (

View File

@ -1,13 +1,30 @@
import React from 'react'; import React, { useEffect } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import InputFileModal from '../InputFileModal'; import InputFileModal from '../InputFileModal';
import InputUploadURL from '../InputUploadURL';
import ModalNavWrapper from '../ModalNavWrapper'; import ModalNavWrapper from '../ModalNavWrapper';
import ModalSection from '../ModalSection'; import ModalSection from '../ModalSection';
const UploadForm = ({ addFilesToUpload }) => { const UploadForm = ({
addFilesToUpload,
filesToDownload,
onChange,
setShouldDisplayNextButton,
}) => {
useEffect(() => {
return () => {
setShouldDisplayNextButton(false);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const handleClick = to => {
setShouldDisplayNextButton(to === 'url');
};
const links = [ const links = [
{ to: 'computer', label: 'computer', isDisabled: false }, { to: 'computer', label: 'computer', isDisabled: false, onClick: handleClick },
{ to: 'url', label: 'url', isDisabled: true }, { to: 'url', label: 'url', isDisabled: false, onClick: handleClick },
]; ];
return ( return (
@ -15,7 +32,7 @@ const UploadForm = ({ addFilesToUpload }) => {
{to => ( {to => (
<ModalSection> <ModalSection>
{to === 'computer' && <InputFileModal onChange={addFilesToUpload} />} {to === 'computer' && <InputFileModal onChange={addFilesToUpload} />}
{to === 'url' && <div>COMING SOON</div>} {to === 'url' && <InputUploadURL onChange={onChange} value={filesToDownload} />}
</ModalSection> </ModalSection>
)} )}
</ModalNavWrapper> </ModalNavWrapper>
@ -24,10 +41,16 @@ const UploadForm = ({ addFilesToUpload }) => {
UploadForm.defaultProps = { UploadForm.defaultProps = {
addFilesToUpload: () => {}, addFilesToUpload: () => {},
filesToDownload: [],
onChange: () => {},
setShouldDisplayNextButton: () => {},
}; };
UploadForm.propTypes = { UploadForm.propTypes = {
addFilesToUpload: PropTypes.func, addFilesToUpload: PropTypes.func,
filesToDownload: PropTypes.arrayOf(PropTypes.string),
onChange: PropTypes.func,
setShouldDisplayNextButton: PropTypes.func,
}; };
export default UploadForm; export default UploadForm;

View File

@ -11,13 +11,14 @@ const RowItem = ({
fileInfo, fileInfo,
hasError, hasError,
errorMessage, errorMessage,
isDownloading,
isUploading, isUploading,
onClick, onClick,
onClickDeleteFileToUpload, onClickDeleteFileToUpload,
onClickEdit, onClickEdit,
originalIndex, originalIndex,
}) => { }) => {
const url = URL.createObjectURL(file); const url = file ? URL.createObjectURL(file) : null;
const handleClick = () => { const handleClick = () => {
onClick(originalIndex); onClick(originalIndex);
@ -31,7 +32,14 @@ const RowItem = ({
onClickEdit(originalIndex); onClickEdit(originalIndex);
}; };
const fileSize = file.mime ? file.type : file.size / 1000; let fileSize = null;
if (file) {
fileSize = file.mime ? file.type : file.size / 1000;
}
const shouldDisplayControls = !isUploading && !isDownloading && file !== null;
const shouldDisplayTrashIcon = file === null && hasError;
return ( return (
<div className="col-xs-12 col-md-6 col-xl-3" key={originalIndex}> <div className="col-xs-12 col-md-6 col-xl-3" key={originalIndex}>
@ -40,14 +48,20 @@ const RowItem = ({
errorMessage={errorMessage} errorMessage={errorMessage}
hasError={hasError} hasError={hasError}
hasIcon hasIcon
type={file.type} type={file ? file.type : null}
size={fileSize} size={fileSize}
url={url} url={url}
{...fileInfo} {...fileInfo}
withFileCaching={false} withFileCaching={false}
withoutFileInfo={isDownloading || (file === null && hasError)}
> >
{isUploading && <InfiniteLoadingIndicator onClick={handleClick} />} {(isUploading || isDownloading) && <InfiniteLoadingIndicator onClick={handleClick} />}
{!isUploading && ( {shouldDisplayTrashIcon && (
<CardControlsWrapper className="card-control-wrapper">
<CardControl title="delete" onClick={handleClickDelete} type="trash-alt" small />
</CardControlsWrapper>
)}
{shouldDisplayControls && (
<CardControlsWrapper className="card-control-wrapper"> <CardControlsWrapper className="card-control-wrapper">
<CardControl title="delete" onClick={handleClickDelete} type="trash-alt" small /> <CardControl title="delete" onClick={handleClickDelete} type="trash-alt" small />
<CardControl title="edit" onClick={handleClickEdit} small /> <CardControl title="edit" onClick={handleClickEdit} small />
@ -59,16 +73,19 @@ const RowItem = ({
}; };
RowItem.defaultProps = { RowItem.defaultProps = {
file: null,
errorMessage: null, errorMessage: null,
isDownloading: false,
}; };
RowItem.propTypes = { RowItem.propTypes = {
file: PropTypes.object.isRequired, file: PropTypes.object,
fileInfo: PropTypes.shape({ fileInfo: PropTypes.shape({
name: PropTypes.string.isRequired, name: PropTypes.string.isRequired,
}).isRequired, }).isRequired,
hasError: PropTypes.bool.isRequired, hasError: PropTypes.bool.isRequired,
errorMessage: PropTypes.string, errorMessage: PropTypes.string,
isDownloading: PropTypes.bool,
isUploading: PropTypes.bool.isRequired, isUploading: PropTypes.bool.isRequired,
onClick: PropTypes.func.isRequired, onClick: PropTypes.func.isRequired,
onClickDeleteFileToUpload: PropTypes.func.isRequired, onClickDeleteFileToUpload: PropTypes.func.isRequired,

View File

@ -2,7 +2,7 @@ import React, { useEffect, useState, useRef, memo } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { Modal, ModalFooter, PopUpWarning, useGlobalContext, request } from 'strapi-helper-plugin'; import { Modal, ModalFooter, PopUpWarning, useGlobalContext, request } from 'strapi-helper-plugin';
import { Button } from '@buffetjs/core'; import { Button } from '@buffetjs/core';
import { isEmpty } from 'lodash';
import { getRequestUrl, getTrad } from '../../utils'; import { getRequestUrl, getTrad } from '../../utils';
import ModalHeader from '../../components/ModalHeader'; import ModalHeader from '../../components/ModalHeader';
import pluginId from '../../pluginId'; import pluginId from '../../pluginId';
@ -12,16 +12,20 @@ import useModalContext from '../../hooks/useModalContext';
const InputModalStepper = ({ isOpen, onToggle, onInputMediaChange }) => { const InputModalStepper = ({ isOpen, onToggle, onInputMediaChange }) => {
const { formatMessage } = useGlobalContext(); const { formatMessage } = useGlobalContext();
const [shouldDeleteFile, setShouldDeleteFile] = useState(false); const [shouldDeleteFile, setShouldDeleteFile] = useState(false);
const [displayNextButton, setDisplayNextButton] = useState(false);
const { const {
addFilesToUpload, addFilesToUpload,
currentStep, currentStep,
downloadFiles,
fetchMediaLib, fetchMediaLib,
filesToDownload,
filesToUpload, filesToUpload,
fileToEdit, fileToEdit,
goTo, goTo,
handleAbortUpload, handleAbortUpload,
handleCancelFileToUpload, handleCancelFileToUpload,
handleCleanFilesError, handleCleanFilesError,
handleClickNextButton,
handleClose, handleClose,
handleEditExistingFile, handleEditExistingFile,
handleFileSelection, handleFileSelection,
@ -56,29 +60,43 @@ const InputModalStepper = ({ isOpen, onToggle, onInputMediaChange }) => {
}; };
useEffect(() => { useEffect(() => {
if (currentStep === 'upload' && filesToUploadLength === 0) { if (currentStep === 'upload') {
// Go to the modal list view when file uploading is over // Go to the modal list view when file uploading is over
if (filesToUploadLength === 0) {
goToList(); goToList();
} else {
downloadFiles();
}
} }
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [filesToUploadLength, currentStep]); }, [filesToUploadLength, currentStep]);
const goToList = () => {
fetchMediaLib();
goTo('list');
};
const handleConfirmDeleteFile = () => {
setShouldDeleteFile(true);
toggleModalWarning();
};
const addFilesToUploadList = ({ target: { value } }) => { const addFilesToUploadList = ({ target: { value } }) => {
addFilesToUpload({ target: { value } }); addFilesToUpload({ target: { value } });
goNext(); goNext();
}; };
const goBack = () => {
goTo(prev);
};
const goNext = () => {
if (next === null) {
onToggle();
return;
}
goTo(next);
};
const goToList = () => {
fetchMediaLib();
goTo('list');
};
const handleClickDeleteFile = async () => { const handleClickDeleteFile = async () => {
toggleModalWarning(); toggleModalWarning();
}; };
@ -93,6 +111,16 @@ const InputModalStepper = ({ isOpen, onToggle, onInputMediaChange }) => {
} }
}; };
const handleCloseModal = () => {
setDisplayNextButton(false);
handleClose();
};
const handleConfirmDeleteFile = () => {
setShouldDeleteFile(true);
toggleModalWarning();
};
const handleGoToAddBrowseFiles = () => { const handleGoToAddBrowseFiles = () => {
handleCleanFilesError(); handleCleanFilesError();
@ -187,25 +215,12 @@ const InputModalStepper = ({ isOpen, onToggle, onInputMediaChange }) => {
onToggle(); onToggle();
}; };
const goBack = () => { const shouldDisplayNextButton = currentStep === 'browse' && displayNextButton;
goTo(prev); const isFinishButtonDisabled = filesToUpload.some(file => file.isDownloading);
};
// FIXME: when back button needed
// eslint-disable-next-line no-unused-vars
const goNext = () => {
if (next === null) {
onToggle();
return;
}
goTo(next);
};
return ( return (
<> <>
<Modal isOpen={isOpen} onToggle={handleToggle} onClosed={handleClose}> <Modal isOpen={isOpen} onToggle={handleToggle} onClosed={handleCloseModal}>
{/* header title */} {/* header title */}
<ModalHeader <ModalHeader
goBack={goBack} goBack={goBack}
@ -218,6 +233,7 @@ const InputModalStepper = ({ isOpen, onToggle, onInputMediaChange }) => {
<Component <Component
addFilesToUpload={addFilesToUploadList} addFilesToUpload={addFilesToUploadList}
components={components} components={components}
filesToDownload={filesToDownload}
filesToUpload={filesToUpload} filesToUpload={filesToUpload}
fileToEdit={fileToEdit} fileToEdit={fileToEdit}
isEditingUploadedFile={currentStep === 'edit'} isEditingUploadedFile={currentStep === 'edit'}
@ -238,6 +254,7 @@ const InputModalStepper = ({ isOpen, onToggle, onInputMediaChange }) => {
toggleDisableForm={handleFormDisabled} toggleDisableForm={handleFormDisabled}
onToggle={handleToggle} onToggle={handleToggle}
setCropResult={handleSetCropResult} setCropResult={handleSetCropResult}
setShouldDisplayNextButton={setDisplayNextButton}
withBackButton={withBackButton} withBackButton={withBackButton}
/> />
)} )}
@ -248,7 +265,12 @@ const InputModalStepper = ({ isOpen, onToggle, onInputMediaChange }) => {
{formatMessage({ id: 'app.components.Button.cancel' })} {formatMessage({ id: 'app.components.Button.cancel' })}
</Button> </Button>
{currentStep === 'upload' && ( {currentStep === 'upload' && (
<Button type="button" color="success" onClick={handleUploadFiles}> <Button
type="button"
color="success"
onClick={handleUploadFiles}
disabled={isFinishButtonDisabled}
>
{formatMessage( {formatMessage(
{ {
id: getTrad( id: getTrad(
@ -261,6 +283,16 @@ const InputModalStepper = ({ isOpen, onToggle, onInputMediaChange }) => {
)} )}
</Button> </Button>
)} )}
{shouldDisplayNextButton && (
<Button
type="button"
color="primary"
onClick={handleClickNextButton}
disabled={isEmpty(filesToDownload)}
>
{formatMessage({ id: getTrad('button.next') })}
</Button>
)}
{currentStep === 'edit-new' && ( {currentStep === 'edit-new' && (
<Button color="success" type="button" onClick={handleSubmitEditNewFile}> <Button color="success" type="button" onClick={handleSubmitEditNewFile}>
{formatMessage({ id: 'form.button.finish' })} {formatMessage({ id: 'form.button.finish' })}

View File

@ -2,12 +2,20 @@ import React, { useReducer, useEffect } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { request, generateSearchFromFilters } from 'strapi-helper-plugin'; import { request, generateSearchFromFilters } from 'strapi-helper-plugin';
import { get } from 'lodash'; import { get } from 'lodash';
import axios from 'axios';
import pluginId from '../../pluginId'; import pluginId from '../../pluginId';
import { getRequestUrl, compactParams, createNewFilesToUploadArray } from '../../utils'; import {
getFilesToDownload,
getRequestUrl,
compactParams,
createNewFilesToUploadArray,
} from '../../utils';
import InputModalStepperContext from '../../contexts/InputModal/InputModalDataManager'; import InputModalStepperContext from '../../contexts/InputModal/InputModalDataManager';
import init from './init'; import init from './init';
import reducer, { initialState } from './reducer'; import reducer, { initialState } from './reducer';
/* eslint-disable indent */
const InputModalStepperProvider = ({ const InputModalStepperProvider = ({
allowedTypes, allowedTypes,
children, children,
@ -47,6 +55,48 @@ const InputModalStepperProvider = ({
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [isOpen, params]); }, [isOpen, params]);
const downloadFiles = 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,
})
.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 handleRemoveFileToUpload = fileIndex => { const handleRemoveFileToUpload = fileIndex => {
dispatch({ dispatch({
type: 'REMOVE_FILE_TO_UPLOAD', type: 'REMOVE_FILE_TO_UPLOAD',
@ -54,11 +104,26 @@ const InputModalStepperProvider = ({
}); });
}; };
const handleFileToEditChange = ({ target: { name, value } }) => { const handleClickNextButton = () => {
dispatch({ dispatch({
type: 'ON_CHANGE', type: 'ADD_URLS_TO_FILES_TO_UPLOAD',
nextStep: 'upload',
});
};
const handleFileToEditChange = ({ target: { name, value } }) => {
let val = value;
let type = 'ON_CHANGE';
if (name === 'url') {
val = value.split('\n');
type = 'ON_CHANGE_URLS_TO_DOWNLOAD';
}
dispatch({
type,
keys: name, keys: name,
value, value: val,
}); });
}; };
@ -186,8 +251,16 @@ const InputModalStepperProvider = ({
const handleCancelFileToUpload = fileIndex => { const handleCancelFileToUpload = fileIndex => {
const fileToCancel = get(filesToUpload, fileIndex, {}); const fileToCancel = get(filesToUpload, fileIndex, {});
const { source } = fileToCancel;
// Cancel upload // Cancel upload
if (source) {
// Cancel dowload file upload with axios
source.cancel('Operation canceled by the user.');
} else {
// Cancel uplodad file with fetch
fileToCancel.abortController.abort(); fileToCancel.abortController.abort();
}
handleRemoveFileToUpload(fileIndex); handleRemoveFileToUpload(fileIndex);
}; };
@ -306,11 +379,13 @@ const InputModalStepperProvider = ({
value={{ value={{
...reducerState, ...reducerState,
addFilesToUpload, addFilesToUpload,
downloadFiles,
fetchMediaLib, fetchMediaLib,
goTo, goTo,
handleAbortUpload, handleAbortUpload,
handleAllFilesSelection, handleAllFilesSelection,
handleCancelFileToUpload, handleCancelFileToUpload,
handleClickNextButton,
handleCleanFilesError, handleCleanFilesError,
handleClose, handleClose,
handleEditExistingFile, handleEditExistingFile,

View File

@ -1,12 +1,17 @@
import produce from 'immer'; import produce from 'immer';
import { intersectionWith, differenceWith, unionWith, set } from 'lodash'; import { intersectionWith, differenceWith, unionWith, set } from 'lodash';
import { createNewFilesToUploadArray, formatFileForEditing } from '../../utils'; import {
createNewFilesToDownloadArray,
createNewFilesToUploadArray,
formatFileForEditing,
} from '../../utils';
const initialState = { const initialState = {
selectedFiles: [], selectedFiles: [],
files: [], files: [],
filesToUpload: [], filesToUpload: [],
filesToDownload: [],
fileToEdit: null, fileToEdit: null,
currentTab: null, currentTab: null,
params: { params: {
@ -24,10 +29,38 @@ const reducer = (state, action) =>
// eslint-disable-next-line consistent-return // eslint-disable-next-line consistent-return
produce(state, draftState => { produce(state, draftState => {
switch (action.type) { switch (action.type) {
case 'ADD_URLS_TO_FILES_TO_UPLOAD': {
draftState.filesToUpload = [
...draftState.filesToUpload,
...createNewFilesToDownloadArray(draftState.filesToDownload, draftState.filesToUpload),
].map((fileToUpload, index) => ({
...fileToUpload,
originalIndex: index,
}));
draftState.currentStep = action.nextStep;
draftState.filesToDownload = [];
break;
}
case 'FILE_DOWNLOADED': {
const index = state.filesToUpload.findIndex(file => file.tempId === action.fileTempId);
draftState.filesToUpload[index] = {
...draftState.filesToUpload[index],
isDownloading: false,
file: action.blob,
};
break;
}
case 'ON_CHANGE': { case 'ON_CHANGE': {
set(draftState.fileToEdit, action.keys.split('.'), action.value); set(draftState.fileToEdit, action.keys.split('.'), action.value);
break; break;
} }
case 'ON_CHANGE_URLS_TO_DOWNLOAD': {
set(draftState, ['filesToDownload'], action.value);
break;
}
case 'GET_DATA_SUCCEEDED': { case 'GET_DATA_SUCCEEDED': {
draftState.files = action.files; draftState.files = action.files;
draftState.count = action.countData.count; draftState.count = action.countData.count;
@ -174,6 +207,18 @@ const reducer = (state, action) =>
); );
break; break;
} }
case 'SET_FILE_TO_DOWNLOAD_ERROR': {
const index = state.filesToUpload.findIndex(file => file.tempId === action.fileTempId);
draftState.filesToUpload[index] = {
...draftState.filesToUpload[index],
isDownloading: false,
hasError: true,
errorMessage: draftState.filesToUpload[index].fileURL,
};
break;
}
case 'SET_FORM_DISABLED': { case 'SET_FORM_DISABLED': {
draftState.isFormDisabled = action.isFormDisabled; draftState.isFormDisabled = action.isFormDisabled;
break; break;

View File

@ -30,6 +30,7 @@ describe('UPLOAD | containers | ModalStepper | reducer', () => {
errorMessage: null, errorMessage: null,
isUploading: false, isUploading: false,
originalIndex: 0, originalIndex: 0,
tempId: null,
}, },
{ {
abortController: new AbortController(), abortController: new AbortController(),
@ -43,6 +44,7 @@ describe('UPLOAD | containers | ModalStepper | reducer', () => {
errorMessage: null, errorMessage: null,
isUploading: false, isUploading: false,
originalIndex: 1, originalIndex: 1,
tempId: null,
}, },
], ],
}; };
@ -74,6 +76,7 @@ describe('UPLOAD | containers | ModalStepper | reducer', () => {
errorMessage: null, errorMessage: null,
isUploading: false, isUploading: false,
originalIndex: 0, originalIndex: 0,
tempId: null,
}, },
], ],
}; };
@ -92,6 +95,7 @@ describe('UPLOAD | containers | ModalStepper | reducer', () => {
errorMessage: null, errorMessage: null,
isUploading: false, isUploading: false,
originalIndex: 0, originalIndex: 0,
tempId: null,
}, },
{ {
abortController: new AbortController(), abortController: new AbortController(),
@ -105,6 +109,7 @@ describe('UPLOAD | containers | ModalStepper | reducer', () => {
errorMessage: null, errorMessage: null,
isUploading: false, isUploading: false,
originalIndex: 1, originalIndex: 1,
tempId: null,
}, },
{ {
abortController: new AbortController(), abortController: new AbortController(),
@ -118,6 +123,7 @@ describe('UPLOAD | containers | ModalStepper | reducer', () => {
errorMessage: null, errorMessage: null,
isUploading: false, isUploading: false,
originalIndex: 2, originalIndex: 2,
tempId: null,
}, },
], ],
}; };
@ -141,6 +147,7 @@ describe('UPLOAD | containers | ModalStepper | reducer', () => {
errorMessage: null, errorMessage: null,
isUploading: false, isUploading: false,
originalIndex: 0, originalIndex: 0,
tempId: null,
}, },
], ],
}; };
@ -154,6 +161,7 @@ describe('UPLOAD | containers | ModalStepper | reducer', () => {
errorMessage: null, errorMessage: null,
isUploading: false, isUploading: false,
originalIndex: 0, originalIndex: 0,
tempId: null,
}, },
], ],
}; };
@ -162,6 +170,188 @@ describe('UPLOAD | containers | ModalStepper | reducer', () => {
}); });
}); });
describe('ADD_URLS_TO_FILES_TO_UPLOAD', () => {
it('should add the files to the empty filesToUpload array and update the current step', () => {
const action = {
type: 'ADD_URLS_TO_FILES_TO_UPLOAD',
nextStep: 'test',
};
const state = {
currentStep: 'browse',
filesToUpload: [],
filesToDownload: ['test', 'test1'],
};
const expected = {
currentStep: 'test',
filesToDownload: [],
filesToUpload: [
{
file: null,
fileInfo: {
alternativeText: '',
caption: '',
name: 'test',
},
originalIndex: 0,
fileURL: 'test',
hasError: false,
errorMessage: null,
isUploading: false,
isDownloading: true,
tempId: 1,
},
{
file: null,
fileInfo: {
alternativeText: '',
caption: '',
name: 'test1',
},
originalIndex: 1,
fileURL: 'test1',
hasError: false,
errorMessage: null,
isUploading: false,
isDownloading: true,
tempId: 2,
},
],
};
const received = reducer(state, action);
expect(received.currentStep).toEqual(expected.currentStep);
expect(received.filesToDownload).toEqual(expected.filesToDownload);
expect(received.filesToUpload).toEqual(
expect.arrayContaining([
expect.objectContaining(expected.filesToUpload[0]),
expect.objectContaining(expected.filesToUpload[1]),
])
);
});
it('should add the files to the (not empty) filesToUpload array and update the current step', () => {
const state = {
currentStep: 'browse',
filesToDownload: ['test2', 'test3'],
filesToUpload: [
{
abortController: new AbortController(),
file: { name: 'test1', ok: true },
fileInfo: {
alternativeText: '',
caption: '',
name: 'test1',
},
hasError: false,
errorMessage: null,
isUploading: false,
originalIndex: 0,
tempId: null,
},
{
file: null,
fileInfo: {
alternativeText: '',
caption: '',
name: 'test1',
},
originalIndex: 1,
fileURL: 'test1',
hasError: false,
errorMessage: null,
isUploading: false,
isDownloading: true,
tempId: 2,
},
],
};
const action = {
type: 'ADD_URLS_TO_FILES_TO_UPLOAD',
nextStep: 'test',
};
const expected = {
currentStep: 'test',
filesToDownload: [],
filesToUpload: [
{
abortController: new AbortController(),
file: { name: 'test1', ok: true },
fileInfo: {
alternativeText: '',
caption: '',
name: 'test1',
},
hasError: false,
errorMessage: null,
isUploading: false,
originalIndex: 0,
tempId: null,
},
{
file: null,
fileInfo: {
alternativeText: '',
caption: '',
name: 'test1',
},
originalIndex: 1,
fileURL: 'test1',
hasError: false,
errorMessage: null,
isUploading: false,
isDownloading: true,
tempId: 2,
},
{
file: null,
fileInfo: {
alternativeText: '',
caption: '',
name: 'test2',
},
originalIndex: 2,
fileURL: 'test2',
hasError: false,
errorMessage: null,
isUploading: false,
isDownloading: true,
tempId: 3,
},
{
file: null,
fileInfo: {
alternativeText: '',
caption: '',
name: 'test3',
},
originalIndex: 3,
fileURL: 'test3',
hasError: false,
errorMessage: null,
isUploading: false,
isDownloading: true,
tempId: 4,
},
],
};
const received = reducer(state, action);
expect(received.currentStep).toEqual(expected.currentStep);
expect(received.filesToDownload).toEqual(expected.filesToDownload);
expect(received.filesToUpload).toEqual(
expect.arrayContaining([
expect.objectContaining(expected.filesToUpload[0]),
expect.objectContaining(expected.filesToUpload[1]),
expect.objectContaining(expected.filesToUpload[2]),
expect.objectContaining(expected.filesToUpload[3]),
])
);
});
});
describe('CLEAN_FILES_ERROR', () => { describe('CLEAN_FILES_ERROR', () => {
it('should not change the filesToUpload property if it is empty', () => { it('should not change the filesToUpload property if it is empty', () => {
const action = { const action = {
@ -243,6 +433,219 @@ describe('UPLOAD | containers | ModalStepper | reducer', () => {
}); });
}); });
describe('FILE_DOWLOADED', () => {
it('should update the corresponding file', () => {
const state = {
currentStep: 'browse',
filesToDownload: [],
filesToUpload: [
{
abortController: new AbortController(),
file: { name: 'test1', ok: true },
fileInfo: {
alternativeText: '',
caption: '',
name: 'test1',
},
hasError: false,
errorMessage: null,
isUploading: false,
originalIndex: 0,
tempId: null,
},
{
file: null,
fileInfo: {
alternativeText: '',
caption: '',
name: 'test1',
},
originalIndex: 1,
fileURL: 'test1',
hasError: false,
errorMessage: null,
isUploading: false,
isDownloading: true,
tempId: 2,
},
],
};
const action = {
type: 'FILE_DOWNLOADED',
fileTempId: 2,
blob: 'test',
};
const expected = {
currentStep: 'browse',
filesToDownload: [],
filesToUpload: [
{
abortController: new AbortController(),
file: { name: 'test1', ok: true },
fileInfo: {
alternativeText: '',
caption: '',
name: 'test1',
},
hasError: false,
errorMessage: null,
isUploading: false,
originalIndex: 0,
tempId: null,
},
{
file: 'test',
fileInfo: {
alternativeText: '',
caption: '',
name: 'test1',
},
originalIndex: 1,
fileURL: 'test1',
hasError: false,
errorMessage: null,
isUploading: false,
isDownloading: false,
tempId: 2,
},
],
};
expect(reducer(state, action)).toEqual(expected);
});
});
describe('ON_CHANGE', () => {
it('should change the data correctly', () => {
const action = {
type: 'ON_CHANGE',
keys: 'test',
value: 'test 1',
};
const state = {
fileToEdit: {
test: 'test',
isUploading: true,
},
currentStep: 'test',
};
const expected = {
fileToEdit: {
test: 'test 1',
isUploading: true,
},
currentStep: 'test',
};
expect(reducer(state, action)).toEqual(expected);
});
});
describe('ON_CHANGE_URLS_TO_DOWNLOAD', () => {
it('should change the data correctly', () => {
const action = {
type: 'ON_CHANGE_URLS_TO_DOWNLOAD',
keys: 'test',
value: ['test 1', 'test 2'],
};
const state = {
filesToDownload: [],
currentStep: 'test',
};
const expected = {
filesToDownload: ['test 1', 'test 2'],
currentStep: 'test',
};
expect(reducer(state, action)).toEqual(expected);
});
});
describe('SET_FILE_TO_DOWNLOAD_ERROR', () => {
it('should update the specified file error', () => {
const state = {
currentStep: 'browse',
filesToDownload: [],
filesToUpload: [
{
abortController: new AbortController(),
file: { name: 'test1', ok: true },
fileInfo: {
alternativeText: '',
caption: '',
name: 'test1',
},
hasError: false,
errorMessage: null,
isUploading: false,
originalIndex: 0,
tempId: null,
},
{
file: null,
fileInfo: {
alternativeText: '',
caption: '',
name: 'test1',
},
originalIndex: 1,
fileURL: 'test1',
hasError: false,
errorMessage: null,
isUploading: false,
isDownloading: true,
tempId: 2,
},
],
};
const action = {
type: 'SET_FILE_TO_DOWNLOAD_ERROR',
fileTempId: 2,
};
const expected = {
currentStep: 'browse',
filesToDownload: [],
filesToUpload: [
{
abortController: new AbortController(),
file: { name: 'test1', ok: true },
fileInfo: {
alternativeText: '',
caption: '',
name: 'test1',
},
hasError: false,
errorMessage: null,
isUploading: false,
originalIndex: 0,
tempId: null,
},
{
file: null,
fileInfo: {
alternativeText: '',
caption: '',
name: 'test1',
},
originalIndex: 1,
fileURL: 'test1',
hasError: true,
errorMessage: 'test1',
isUploading: false,
isDownloading: false,
tempId: 2,
},
],
};
expect(reducer(state, action)).toEqual(expected);
});
});
describe('TOGGLE_SELECT_ALL', () => { describe('TOGGLE_SELECT_ALL', () => {
it('should select all files', () => { it('should select all files', () => {
const action = { const action = {
@ -1100,6 +1503,7 @@ describe('UPLOAD | containers | ModalStepper | reducer', () => {
const expected = { const expected = {
selectedFiles: [], selectedFiles: [],
files: [], files: [],
filesToDownload: [],
filesToUpload: [], filesToUpload: [],
fileToEdit: null, fileToEdit: null,
currentTab: null, currentTab: null,

View File

@ -1,10 +1,11 @@
import React, { useEffect, useState, useReducer, useRef } from 'react'; import React, { useEffect, useState, useReducer, useRef } from 'react';
import axios from 'axios';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { isEqual, get } from 'lodash'; import { isEqual, isEmpty, get } from 'lodash';
import { Modal, ModalFooter, PopUpWarning, useGlobalContext, request } from 'strapi-helper-plugin'; import { Modal, ModalFooter, PopUpWarning, useGlobalContext, request } from 'strapi-helper-plugin';
import { Button } from '@buffetjs/core'; import { Button } from '@buffetjs/core';
import pluginId from '../../pluginId'; import pluginId from '../../pluginId';
import { getTrad } from '../../utils'; import { getFilesToDownload, getTrad } from '../../utils';
import ModalHeader from '../../components/ModalHeader'; import ModalHeader from '../../components/ModalHeader';
import stepper from './stepper'; import stepper from './stepper';
import init from './init'; import init from './init';
@ -22,20 +23,29 @@ const ModalStepper = ({
const [isWarningDeleteOpen, setIsWarningDeleteOpen] = useState(false); const [isWarningDeleteOpen, setIsWarningDeleteOpen] = useState(false);
const [shouldDeleteFile, setShouldDeleteFile] = useState(false); const [shouldDeleteFile, setShouldDeleteFile] = useState(false);
const [isFormDisabled, setIsFormDisabled] = useState(false); const [isFormDisabled, setIsFormDisabled] = useState(false);
const [displayNextButton, setDisplayNextButton] = useState(false);
const [reducerState, dispatch] = useReducer(reducer, initialState, init); const [reducerState, dispatch] = useReducer(reducer, initialState, init);
const { currentStep, fileToEdit, filesToUpload } = reducerState.toJS(); const { currentStep, fileToEdit, filesToDownload, filesToUpload } = reducerState.toJS();
const { Component, components, headerBreadcrumbs, next, prev, withBackButton } = stepper[ const { Component, components, headerBreadcrumbs, next, prev, withBackButton } = stepper[
currentStep currentStep
]; ];
const filesToUploadLength = filesToUpload.length; const filesToUploadLength = filesToUpload.length;
const toggleRef = useRef(onToggle); const toggleRef = useRef(onToggle);
const editModalRef = useRef(); const editModalRef = useRef();
const downloadFilesRef = useRef();
useEffect(() => { useEffect(() => {
if (currentStep === 'upload' && filesToUploadLength === 0) { if (currentStep === 'upload') {
// Close the modal
if (filesToUploadLength === 0) {
// Passing true to the onToggle prop will refetch the data when the modal closes // Passing true to the onToggle prop will refetch the data when the modal closes
toggleRef.current(true); toggleRef.current(true);
} else {
// Download files from url
downloadFilesRef.current();
} }
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [filesToUploadLength, currentStep]); }, [filesToUploadLength, currentStep]);
useEffect(() => { useEffect(() => {
@ -63,6 +73,49 @@ const ModalStepper = ({
goTo(next); 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 handleAbortUpload = () => {
const { abortController } = fileToEdit; const { abortController } = fileToEdit;
@ -75,9 +128,16 @@ const ModalStepper = ({
const handleCancelFileToUpload = fileOriginalIndex => { const handleCancelFileToUpload = fileOriginalIndex => {
const fileToCancel = filesToUpload.find(file => file.originalIndex === fileOriginalIndex); const fileToCancel = filesToUpload.find(file => file.originalIndex === fileOriginalIndex);
const { source } = fileToCancel;
// Cancel upload // Cancel
if (source) {
// Cancel dowload file upload with axios
source.cancel('Operation canceled by the user.');
} else {
// Cancel upload with fetch
fileToCancel.abortController.abort(); fileToCancel.abortController.abort();
}
dispatch({ dispatch({
type: 'REMOVE_FILE_TO_UPLOAD', type: 'REMOVE_FILE_TO_UPLOAD',
@ -86,10 +146,18 @@ const ModalStepper = ({
}; };
const handleChange = ({ target: { name, value } }) => { const handleChange = ({ target: { name, value } }) => {
let val = value;
let type = 'ON_CHANGE';
if (name === 'url') {
val = value.split('\n');
type = 'ON_CHANGE_URLS_TO_DOWNLOAD';
}
dispatch({ dispatch({
type: 'ON_CHANGE', type,
keys: name, keys: name,
value, value: val,
}); });
}; };
@ -98,6 +166,14 @@ const ModalStepper = ({
toggleModalWarning(); toggleModalWarning();
}; };
const handleClickNextButton = () => {
// Navigate to next step
dispatch({
type: 'ADD_URLS_TO_FILES_TO_UPLOAD',
nextStep: next,
});
};
const handleClickDeleteFile = async () => { const handleClickDeleteFile = async () => {
toggleModalWarning(); toggleModalWarning();
}; };
@ -120,6 +196,7 @@ const ModalStepper = ({
const handleClose = () => { const handleClose = () => {
onClosed(); onClosed();
setIsFormDisabled(false); setIsFormDisabled(false);
setDisplayNextButton(false);
dispatch({ dispatch({
type: 'RESET_PROPS', type: 'RESET_PROPS',
@ -297,8 +374,6 @@ const ModalStepper = ({
goTo(prev); goTo(prev);
}; };
// FIXME: when back button needed
// eslint-disable-next-line no-unused-vars
const goNext = () => { const goNext = () => {
if (next === null) { if (next === null) {
onToggle(); onToggle();
@ -320,6 +395,9 @@ const ModalStepper = ({
setIsWarningDeleteOpen(prev => !prev); setIsWarningDeleteOpen(prev => !prev);
}; };
const shouldDisplayNextButton = currentStep === 'browse' && displayNextButton;
const isFinishButtonDisabled = filesToUpload.some(file => file.isDownloading);
return ( return (
<> <>
<Modal isOpen={isOpen} onToggle={handleToggle} onClosed={handleClose}> <Modal isOpen={isOpen} onToggle={handleToggle} onClosed={handleClose}>
@ -336,6 +414,7 @@ const ModalStepper = ({
onAbortUpload={handleAbortUpload} onAbortUpload={handleAbortUpload}
addFilesToUpload={addFilesToUpload} addFilesToUpload={addFilesToUpload}
fileToEdit={fileToEdit} fileToEdit={fileToEdit}
filesToDownload={filesToDownload}
filesToUpload={filesToUpload} filesToUpload={filesToUpload}
components={components} components={components}
isEditingUploadedFile={currentStep === 'edit'} isEditingUploadedFile={currentStep === 'edit'}
@ -354,6 +433,7 @@ const ModalStepper = ({
toggleDisableForm={setIsFormDisabled} toggleDisableForm={setIsFormDisabled}
ref={currentStep === 'edit' ? editModalRef : null} ref={currentStep === 'edit' ? editModalRef : null}
setCropResult={handleSetCropResult} setCropResult={handleSetCropResult}
setShouldDisplayNextButton={setDisplayNextButton}
withBackButton={withBackButton} withBackButton={withBackButton}
/> />
)} )}
@ -363,8 +443,23 @@ const ModalStepper = ({
<Button type="button" color="cancel" onClick={handleToggle}> <Button type="button" color="cancel" onClick={handleToggle}>
{formatMessage({ id: 'app.components.Button.cancel' })} {formatMessage({ id: 'app.components.Button.cancel' })}
</Button> </Button>
{shouldDisplayNextButton && (
<Button
type="button"
color="primary"
onClick={handleClickNextButton}
disabled={isEmpty(filesToDownload)}
>
{formatMessage({ id: getTrad('button.next') })}
</Button>
)}
{currentStep === 'upload' && ( {currentStep === 'upload' && (
<Button type="button" color="success" onClick={handleUploadFiles}> <Button
type="button"
color="success"
onClick={handleUploadFiles}
disabled={isFinishButtonDisabled}
>
{formatMessage( {formatMessage(
{ {
id: getTrad( id: getTrad(

View File

@ -1,9 +1,11 @@
import { fromJS } from 'immutable'; import { fromJS } from 'immutable';
import createNewFilesToUploadArray from '../../utils/createNewFilesToUploadArray';
import { createNewFilesToDownloadArray, createNewFilesToUploadArray } from '../../utils';
const initialState = fromJS({ const initialState = fromJS({
currentStep: 'browse', currentStep: 'browse',
filesToUpload: [], filesToUpload: [],
filesToDownload: [],
fileToEdit: null, fileToEdit: null,
}); });
@ -17,18 +19,45 @@ const reducer = (state, action) => {
.map((data, index) => data.set('originalIndex', index)) .map((data, index) => data.set('originalIndex', index))
) )
.update('currentStep', () => action.nextStep); .update('currentStep', () => action.nextStep);
case 'ADD_URLS_TO_FILES_TO_UPLOAD':
return state
.update('filesToUpload', list =>
list
.concat(
fromJS(createNewFilesToDownloadArray(state.get('filesToDownload'), list.toJS()))
)
.map((data, index) => data.set('originalIndex', index))
)
.update('currentStep', () => action.nextStep)
.update('filesToDownload', () => fromJS([]));
case 'CLEAN_FILES_ERROR': case 'CLEAN_FILES_ERROR':
return state.update('filesToUpload', list => return state.update('filesToUpload', list =>
list.map(data => { list.map(data => {
if (data.get('tempId')) {
return data;
}
return data.set('hasError', false).set('errorMessage', null); return data.set('hasError', false).set('errorMessage', null);
}) })
); );
case 'FILE_DOWNLOADED':
return state.updateIn(['filesToUpload'], list => {
return list.map(file => {
if (file.get('tempId') === action.fileTempId) {
return file.update('isDownloading', () => false).update('file', () => action.blob);
}
return file;
});
});
case 'GO_TO': case 'GO_TO':
return state.update('currentStep', () => action.to); return state.update('currentStep', () => action.to);
case 'INIT_FILE_TO_EDIT': case 'INIT_FILE_TO_EDIT':
return state.update('fileToEdit', () => fromJS(action.fileToEdit)); return state.update('fileToEdit', () => fromJS(action.fileToEdit));
case 'ON_ABORT_UPLOAD': case 'ON_ABORT_UPLOAD':
return state.updateIn(['fileToEdit', 'isUploading'], () => false); return state.updateIn(['fileToEdit', 'isUploading'], () => false);
case 'ON_CHANGE_URLS_TO_DOWNLOAD':
return state.updateIn(['filesToDownload'], () => fromJS(action.value));
case 'ON_CHANGE': case 'ON_CHANGE':
return state.updateIn(['fileToEdit', ...action.keys.split('.')], () => action.value); return state.updateIn(['fileToEdit', ...action.keys.split('.')], () => action.value);
case 'ON_SUBMIT_EDIT_NEW_FILE': { case 'ON_SUBMIT_EDIT_NEW_FILE': {
@ -64,6 +93,19 @@ const reducer = (state, action) => {
return data; return data;
}); });
}); });
case 'SET_FILE_TO_DOWNLOAD_ERROR':
return state.update('filesToUpload', list => {
return list.map(file => {
if (file.get('tempId') === action.fileTempId) {
return file
.update('isDownloading', () => false)
.update('hasError', () => true)
.update('errorMessage', () => file.get('fileURL'));
}
return file;
});
});
case 'SET_FILE_TO_EDIT': case 'SET_FILE_TO_EDIT':
return state.update('fileToEdit', () => state.getIn(['filesToUpload', action.fileIndex])); return state.update('fileToEdit', () => state.getIn(['filesToUpload', action.fileIndex]));
case 'SET_FILES_UPLOADING_STATE': case 'SET_FILES_UPLOADING_STATE':

View File

@ -44,6 +44,7 @@ describe('UPLOAD | containers | ModalStepper | reducer', () => {
errorMessage: null, errorMessage: null,
isUploading: false, isUploading: false,
originalIndex: 0, originalIndex: 0,
tempId: null,
}, },
{ {
abortController: new AbortController(), abortController: new AbortController(),
@ -57,6 +58,7 @@ describe('UPLOAD | containers | ModalStepper | reducer', () => {
errorMessage: null, errorMessage: null,
isUploading: false, isUploading: false,
originalIndex: 1, originalIndex: 1,
tempId: null,
}, },
], ],
}); });
@ -88,6 +90,7 @@ describe('UPLOAD | containers | ModalStepper | reducer', () => {
errorMessage: null, errorMessage: null,
isUploading: false, isUploading: false,
originalIndex: 0, originalIndex: 0,
tempId: null,
}, },
], ],
}); });
@ -106,6 +109,7 @@ describe('UPLOAD | containers | ModalStepper | reducer', () => {
errorMessage: null, errorMessage: null,
isUploading: false, isUploading: false,
originalIndex: 0, originalIndex: 0,
tempId: null,
}, },
{ {
abortController: new AbortController(), abortController: new AbortController(),
@ -119,6 +123,7 @@ describe('UPLOAD | containers | ModalStepper | reducer', () => {
errorMessage: null, errorMessage: null,
isUploading: false, isUploading: false,
originalIndex: 1, originalIndex: 1,
tempId: null,
}, },
{ {
abortController: new AbortController(), abortController: new AbortController(),
@ -132,6 +137,7 @@ describe('UPLOAD | containers | ModalStepper | reducer', () => {
errorMessage: null, errorMessage: null,
isUploading: false, isUploading: false,
originalIndex: 2, originalIndex: 2,
tempId: null,
}, },
], ],
}); });
@ -155,6 +161,7 @@ describe('UPLOAD | containers | ModalStepper | reducer', () => {
errorMessage: null, errorMessage: null,
isUploading: false, isUploading: false,
originalIndex: 0, originalIndex: 0,
tempId: null,
}, },
], ],
}); });
@ -168,6 +175,7 @@ describe('UPLOAD | containers | ModalStepper | reducer', () => {
errorMessage: null, errorMessage: null,
isUploading: false, isUploading: false,
originalIndex: 0, originalIndex: 0,
tempId: null,
}, },
], ],
}); });
@ -176,6 +184,188 @@ describe('UPLOAD | containers | ModalStepper | reducer', () => {
}); });
}); });
describe('ADD_URLS_TO_FILES_TO_UPLOAD', () => {
it('should add the files to the empty filesToUpload array and update the current step', () => {
const action = {
type: 'ADD_URLS_TO_FILES_TO_UPLOAD',
nextStep: 'test',
};
const state = fromJS({
currentStep: 'browse',
filesToUpload: [],
filesToDownload: ['test', 'test1'],
});
const expected = fromJS({
currentStep: 'test',
filesToDownload: [],
filesToUpload: [
{
file: null,
fileInfo: {
alternativeText: '',
caption: '',
name: 'test',
},
originalIndex: 0,
fileURL: 'test',
hasError: false,
errorMessage: null,
isUploading: false,
isDownloading: true,
tempId: 1,
},
{
file: null,
fileInfo: {
alternativeText: '',
caption: '',
name: 'test1',
},
originalIndex: 1,
fileURL: 'test1',
hasError: false,
errorMessage: null,
isUploading: false,
isDownloading: true,
tempId: 2,
},
],
});
const received = reducer(state, action);
expect(received.get('currentStep')).toEqual(expected.get('currentStep'));
expect(received.get('filesToDownload')).toEqual(expected.get('filesToDownload'));
expect(received.get('filesToUpload').toJS()).toEqual(
expect.arrayContaining([
expect.objectContaining(expected.getIn(['filesToUpload', '0']).toJS()),
expect.objectContaining(expected.getIn(['filesToUpload', '1']).toJS()),
])
);
});
it('should add the files to the (not empty) filesToUpload array and update the current step', () => {
const state = fromJS({
currentStep: 'browse',
filesToDownload: ['test2', 'test3'],
filesToUpload: [
{
abortController: new AbortController(),
file: { name: 'test1', ok: true },
fileInfo: {
alternativeText: '',
caption: '',
name: 'test1',
},
hasError: false,
errorMessage: null,
isUploading: false,
originalIndex: 0,
tempId: null,
},
{
file: null,
fileInfo: {
alternativeText: '',
caption: '',
name: 'test1',
},
originalIndex: 1,
fileURL: 'test1',
hasError: false,
errorMessage: null,
isUploading: false,
isDownloading: true,
tempId: 2,
},
],
});
const action = {
type: 'ADD_URLS_TO_FILES_TO_UPLOAD',
nextStep: 'test',
};
const expected = fromJS({
currentStep: 'test',
filesToDownload: [],
filesToUpload: [
{
abortController: new AbortController(),
file: { name: 'test1', ok: true },
fileInfo: {
alternativeText: '',
caption: '',
name: 'test1',
},
hasError: false,
errorMessage: null,
isUploading: false,
originalIndex: 0,
tempId: null,
},
{
file: null,
fileInfo: {
alternativeText: '',
caption: '',
name: 'test1',
},
originalIndex: 1,
fileURL: 'test1',
hasError: false,
errorMessage: null,
isUploading: false,
isDownloading: true,
tempId: 2,
},
{
file: null,
fileInfo: {
alternativeText: '',
caption: '',
name: 'test2',
},
originalIndex: 2,
fileURL: 'test2',
hasError: false,
errorMessage: null,
isUploading: false,
isDownloading: true,
tempId: 3,
},
{
file: null,
fileInfo: {
alternativeText: '',
caption: '',
name: 'test3',
},
originalIndex: 3,
fileURL: 'test3',
hasError: false,
errorMessage: null,
isUploading: false,
isDownloading: true,
tempId: 4,
},
],
});
const received = reducer(state, action);
expect(received.get('currentStep')).toEqual(expected.get('currentStep'));
expect(received.get('filesToDownload')).toEqual(expected.get('filesToDownload'));
expect(received.get('filesToUpload').toJS()).toEqual(
expect.arrayContaining([
expect.objectContaining(expected.getIn(['filesToUpload', '0']).toJS()),
expect.objectContaining(expected.getIn(['filesToUpload', '1']).toJS()),
expect.objectContaining(expected.getIn(['filesToUpload', '2']).toJS()),
expect.objectContaining(expected.getIn(['filesToUpload', '3']).toJS()),
])
);
});
});
describe('CLEAN_FILES_ERROR', () => { describe('CLEAN_FILES_ERROR', () => {
it('should not change the filesToUpload property if it is empty', () => { it('should not change the filesToUpload property if it is empty', () => {
const action = { const action = {
@ -255,6 +445,159 @@ describe('UPLOAD | containers | ModalStepper | reducer', () => {
expect(reducer(state, action)).toEqual(expected); expect(reducer(state, action)).toEqual(expected);
}); });
it('should not change the data containing a defined tempId key', () => {
const action = {
type: 'CLEAN_FILES_ERROR',
};
const state = fromJS({
currentStep: 'test',
filesToUpload: [
{
abortController: new AbortController(),
file: { ok: true },
hasError: true,
errorMessage: 'error1',
isUploading: false,
originalIndex: 0,
},
{
abortController: new AbortController(),
file: { test: true },
hasError: true,
errorMessage: 'error2',
isUploading: false,
originalIndex: 1,
tempId: 1,
},
{
abortController: new AbortController(),
file: { test: false },
hasError: true,
errorMessage: 'error3',
isUploading: false,
originalIndex: 2,
},
],
});
const expected = fromJS({
currentStep: 'test',
filesToUpload: [
{
abortController: new AbortController(),
file: { ok: true },
hasError: false,
errorMessage: null,
isUploading: false,
originalIndex: 0,
},
{
abortController: new AbortController(),
file: { test: true },
hasError: true,
errorMessage: 'error2',
isUploading: false,
originalIndex: 1,
tempId: 1,
},
{
abortController: new AbortController(),
file: { test: false },
hasError: false,
errorMessage: null,
isUploading: false,
originalIndex: 2,
},
],
});
expect(reducer(state, action)).toEqual(expected);
});
});
describe('FILE_DOWLOADED', () => {
it('should update the corresponding file', () => {
const state = fromJS({
currentStep: 'browse',
filesToDownload: [],
filesToUpload: [
{
abortController: new AbortController(),
file: { name: 'test1', ok: true },
fileInfo: {
alternativeText: '',
caption: '',
name: 'test1',
},
hasError: false,
errorMessage: null,
isUploading: false,
originalIndex: 0,
tempId: null,
},
{
file: null,
fileInfo: {
alternativeText: '',
caption: '',
name: 'test1',
},
originalIndex: 1,
fileURL: 'test1',
hasError: false,
errorMessage: null,
isUploading: false,
isDownloading: true,
tempId: 2,
},
],
});
const action = {
type: 'FILE_DOWNLOADED',
fileTempId: 2,
blob: 'test',
};
const expected = fromJS({
currentStep: 'browse',
filesToDownload: [],
filesToUpload: [
{
abortController: new AbortController(),
file: { name: 'test1', ok: true },
fileInfo: {
alternativeText: '',
caption: '',
name: 'test1',
},
hasError: false,
errorMessage: null,
isUploading: false,
originalIndex: 0,
tempId: null,
},
{
file: 'test',
fileInfo: {
alternativeText: '',
caption: '',
name: 'test1',
},
originalIndex: 1,
fileURL: 'test1',
hasError: false,
errorMessage: null,
isUploading: false,
isDownloading: false,
tempId: 2,
},
],
});
expect(reducer(state, action)).toEqual(expected);
});
}); });
describe('GO_TO', () => { describe('GO_TO', () => {
@ -347,6 +690,26 @@ describe('UPLOAD | containers | ModalStepper | reducer', () => {
}); });
}); });
describe('ON_CHANGE_URLS_TO_DOWNLOAD', () => {
it('should change the data correctly', () => {
const action = {
type: 'ON_CHANGE_URLS_TO_DOWNLOAD',
keys: 'test',
value: ['test 1', 'test 2'],
};
const state = fromJS({
filesToDownload: [],
currentStep: 'test',
});
const expected = fromJS({
filesToDownload: ['test 1', 'test 2'],
currentStep: 'test',
});
expect(reducer(state, action)).toEqual(expected);
});
});
describe('ON_SUBMIT_EDIT_EXISTING_FILE', () => { describe('ON_SUBMIT_EDIT_EXISTING_FILE', () => {
it('should set the isUploading key to false', () => { it('should set the isUploading key to false', () => {
const action = { const action = {
@ -521,6 +884,7 @@ describe('UPLOAD | containers | ModalStepper | reducer', () => {
const expected = fromJS({ const expected = fromJS({
currentStep: 'browse', currentStep: 'browse',
filesToUpload: [], filesToUpload: [],
filesToDownload: [],
fileToEdit: null, fileToEdit: null,
}); });
@ -555,6 +919,89 @@ describe('UPLOAD | containers | ModalStepper | reducer', () => {
}); });
}); });
describe('SET_FILE_TO_DOWNLOAD_ERROR', () => {
it('should update the specified file error', () => {
const state = fromJS({
currentStep: 'browse',
filesToDownload: [],
filesToUpload: [
{
abortController: new AbortController(),
file: { name: 'test1', ok: true },
fileInfo: {
alternativeText: '',
caption: '',
name: 'test1',
},
hasError: false,
errorMessage: null,
isUploading: false,
originalIndex: 0,
tempId: null,
},
{
file: null,
fileInfo: {
alternativeText: '',
caption: '',
name: 'test1',
},
originalIndex: 1,
fileURL: 'test1',
hasError: false,
errorMessage: null,
isUploading: false,
isDownloading: true,
tempId: 2,
},
],
});
const action = {
type: 'SET_FILE_TO_DOWNLOAD_ERROR',
fileTempId: 2,
};
const expected = fromJS({
currentStep: 'browse',
filesToDownload: [],
filesToUpload: [
{
abortController: new AbortController(),
file: { name: 'test1', ok: true },
fileInfo: {
alternativeText: '',
caption: '',
name: 'test1',
},
hasError: false,
errorMessage: null,
isUploading: false,
originalIndex: 0,
tempId: null,
},
{
file: null,
fileInfo: {
alternativeText: '',
caption: '',
name: 'test1',
},
originalIndex: 1,
fileURL: 'test1',
hasError: true,
errorMessage: 'test1',
isUploading: false,
isDownloading: false,
tempId: 2,
},
],
});
expect(reducer(state, action)).toEqual(expected);
});
});
describe('SET_FILE_ERROR', () => { describe('SET_FILE_ERROR', () => {
it('should update the specified file error', () => { it('should update the specified file error', () => {
const action = { const action = {

View File

@ -1,4 +1,5 @@
{ {
"button.next": "Next",
"checkControl.crop-original": "Crop the original asset", "checkControl.crop-original": "Crop the original asset",
"checkControl.crop-duplicate": "Duplicate & crop the asset", "checkControl.crop-duplicate": "Duplicate & crop the asset",
"control-card.add": "Add", "control-card.add": "Add",
@ -22,6 +23,8 @@
"input.label-normal": "to upload or", "input.label-normal": "to upload or",
"input.placeholder": "Click to select an asset or drag & drop a file in this area", "input.placeholder": "Click to select an asset or drag & drop a file in this area",
"input.button.label": "Browse files", "input.button.label": "Browse files",
"input.url.label": "URL",
"input.url.description": "Separate your URL links by a carriage return.",
"list.assets-empty.title": "There is no asset yet", "list.assets-empty.title": "There is no asset yet",
"list.assets-empty.title-withSearch": "There is no asset with the applied filters", "list.assets-empty.title-withSearch": "There is no asset with the applied filters",
"list.assets-empty.subtitle": "Add a first one to the list.", "list.assets-empty.subtitle": "Add a first one to the list.",

View File

@ -0,0 +1,49 @@
import axios from 'axios';
import { isEmpty } from 'lodash';
const getTempsIds = alreadyUploadedFiles => {
return [...new Set([0, ...alreadyUploadedFiles.map(file => file.tempId).filter(id => !!id)])];
};
const getMax = arr => {
return Math.max.apply(Math, arr) + 1;
};
const createNewFilesToDownloadArray = (filesURLArray, alreadyUploadedFiles) => {
const tempIds = getTempsIds(alreadyUploadedFiles);
const max = getMax(tempIds);
const arrayToReturn = filesURLArray.reduce((acc, current, index) => {
if (isEmpty(current)) {
return acc;
}
const CancelToken = axios.CancelToken;
const abortController = new AbortController();
const source = CancelToken.source();
acc.push({
abortController,
source,
file: null,
fileInfo: {
alternativeText: '',
caption: '',
name: current,
},
fileURL: current,
hasError: false,
errorMessage: null,
isUploading: false,
isDownloading: true,
tempId: max + index,
});
return acc;
}, []);
return arrayToReturn;
};
export default createNewFilesToDownloadArray;
export { getMax, getTempsIds };

View File

@ -14,6 +14,7 @@ const createNewFilesToUploadArray = filesObject => {
hasError: false, hasError: false,
errorMessage: null, errorMessage: null,
isUploading: false, isUploading: false,
tempId: null,
}); });
return acc; return acc;

View File

@ -0,0 +1,5 @@
const getFilesToDownload = files => {
return files.filter(file => file.isDownloading === true);
};
export default getFilesToDownload;

View File

@ -2,12 +2,14 @@ export { default as canDownloadFile } from './canDownloadFile';
export { default as compactParams } from './compactParams'; export { default as compactParams } from './compactParams';
export { default as createFileToDownloadName } from './createFileToDownloadName'; export { default as createFileToDownloadName } from './createFileToDownloadName';
export { default as createMatrix } from './createMatrix'; export { default as createMatrix } from './createMatrix';
export { default as createNewFilesToDownloadArray } from './createNewFilesToDownloadArray';
export { default as createNewFilesToUploadArray } from './createNewFilesToUploadArray'; export { default as createNewFilesToUploadArray } from './createNewFilesToUploadArray';
export { default as formatBytes } from './formatBytes'; export { default as formatBytes } from './formatBytes';
export { default as formatFileForEditing } from './formatFileForEditing'; export { default as formatFileForEditing } from './formatFileForEditing';
export { default as generatePageFromStart } from './generatePageFromStart'; export { default as generatePageFromStart } from './generatePageFromStart';
export { default as generateStartFromPage } from './generateStartFromPage'; export { default as generateStartFromPage } from './generateStartFromPage';
export { default as getExtension } from './getExtension'; export { default as getExtension } from './getExtension';
export { default as getFilesToDownload } from './getFilesToDownload';
export { default as getRequestUrl } from './getRequestUrl'; export { default as getRequestUrl } from './getRequestUrl';
export { default as getTrad } from './getTrad'; export { default as getTrad } from './getTrad';
export { default as getType } from './getType'; export { default as getType } from './getType';

View File

@ -0,0 +1,131 @@
import createNewFilesToDownloadArray, {
getMax,
getTempsIds,
} from '../createNewFilesToDownloadArray';
describe('UPLOAD | utils', () => {
describe('createNewFilesToDownloadArray', () => {
it('should create an array containing the formatted data and filter the empty data', () => {
const dataURLArray = ['', 'un', undefined, 'deux', null, 'trois'];
const dataFilesArray = [
{
abortController: new AbortController(),
file: { name: 'test', ok: true },
fileInfo: {
alternativeText: '',
caption: '',
name: 'test',
},
tempId: null,
hasError: false,
errorMessage: null,
isUploading: false,
},
{
abortController: new AbortController(),
file: { name: 'test', ok: true },
fileInfo: {
alternativeText: '',
caption: '',
name: 'test',
},
tempId: 121,
hasError: false,
errorMessage: null,
isUploading: false,
},
];
const received = createNewFilesToDownloadArray(dataURLArray, dataFilesArray);
expect(received).toEqual(
expect.arrayContaining([
expect.objectContaining({
file: null,
fileInfo: {
alternativeText: '',
caption: '',
name: 'un',
},
fileURL: 'un',
hasError: false,
errorMessage: null,
isUploading: false,
isDownloading: true,
tempId: 123,
}),
expect.objectContaining({
file: null,
fileInfo: {
alternativeText: '',
caption: '',
name: 'deux',
},
fileURL: 'deux',
hasError: false,
errorMessage: null,
isUploading: false,
isDownloading: true,
tempId: 125,
}),
expect.objectContaining({
file: null,
fileInfo: {
alternativeText: '',
caption: '',
name: 'trois',
},
fileURL: 'trois',
hasError: false,
errorMessage: null,
isUploading: false,
isDownloading: true,
tempId: 127,
}),
])
);
});
});
describe('getMax', () => {
it('should return the max of an array + 1', () => {
const data = [0, 12, 1, 121];
const expected = 122;
expect(getMax(data)).toEqual(expected);
});
});
describe('getTempsIds', () => {
it('should add 0 to the initial array', () => {
const data = [];
const expected = [0];
expect(getTempsIds(data)).toEqual(expected);
});
it('should return a unique array of ids and remove the undefined and null values', () => {
const data = [
{
tempId: null,
},
{
tempId: 'a',
},
{
tempId: 0,
},
{},
{
tempId: 1,
},
];
const expected = [0, 'a', 1];
expect(getTempsIds(data)).toEqual(expected);
});
});
});

View File

@ -15,6 +15,7 @@ describe('UPLOAD | containers | ModalStepper | utils', () => {
caption: '', caption: '',
name: 'test', name: 'test',
}, },
tempId: null,
hasError: false, hasError: false,
errorMessage: null, errorMessage: null,
isUploading: false, isUploading: false,

View File

@ -0,0 +1,36 @@
import getFilesToDownload from '../getFilesToDownload';
describe('UPLOAD | utils | getFilesToDownload', () => {
it('should return an array containing only the files that have the isDownloading key to true', () => {
const data = [
{
fileURL: '1',
isDownloading: undefined,
},
{
fileURL: '2',
isDownloading: null,
},
{
fileURL: '3',
isDownloading: 'true',
},
{
fileURL: '4',
isDownloading: false,
},
{
fileURL: '5',
isDownloading: true,
},
];
const expected = [
{
fileURL: '5',
isDownloading: true,
},
];
expect(getFilesToDownload(data)).toEqual(expected);
});
});