Init edit existing file and add copy button

Signed-off-by: soupette <cyril.lpz@gmail.com>
This commit is contained in:
soupette 2020-03-24 07:35:16 +01:00
parent 664db67436
commit 6ae7395bf0
17 changed files with 249 additions and 23 deletions

View File

@ -13,7 +13,9 @@ module.exports = {
],
globals: {
__webpack_public_path__: 'http://localhost:4000',
strapi: {},
strapi: {
backendURL: 'http://localhost:1337',
},
BACKEND_URL: 'http://localhost:1337',
MODE: 'host',
PUBLIC_PATH: '/admin',
@ -26,8 +28,7 @@ module.exports = {
'<rootDir>/test/config/front',
],
moduleNameMapper: {
'.*\\.(css|less|styl|scss|sass)$':
'<rootDir>/test/config/front/mocks/cssModule.js',
'.*\\.(css|less|styl|scss|sass)$': '<rootDir>/test/config/front/mocks/cssModule.js',
'.*\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga|ico)$':
'<rootDir>/test/config/front/mocks/image.js',
},
@ -49,8 +50,6 @@ module.exports = {
'\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$':
'<rootDir>/fileTransformer.js',
},
transformIgnorePatterns: [
'node_modules/(?!(react-dnd|dnd-core|react-dnd-html5-backend)/)',
],
transformIgnorePatterns: ['node_modules/(?!(react-dnd|dnd-core|react-dnd-html5-backend)/)'],
testURL: 'http://localhost:4000/admin',
};

View File

@ -14,12 +14,14 @@ import ErrorMessage from './ErrorMessage';
import Border from './Border';
const Card = ({
id,
checked,
children,
errorMessage,
hasError,
mime,
name,
onClick,
small,
size,
type,
@ -28,8 +30,12 @@ const Card = ({
const fileSize = formatBytes(size, 0);
const fileType = mime || type;
const handleClick = () => {
onClick(id);
};
return (
<Wrapper>
<Wrapper onClick={handleClick}>
<CardImgWrapper checked={checked} small={small}>
<CardPreview hasError={hasError} url={url} type={fileType} />
<Border color={hasError ? 'orange' : 'mediumBlue'} shown={checked || hasError} />
@ -51,9 +57,11 @@ Card.defaultProps = {
checked: false,
children: null,
errorMessage: null,
id: null,
hasError: false,
mime: null,
name: null,
onClick: () => {},
size: 0,
small: false,
type: null,
@ -61,12 +69,14 @@ Card.defaultProps = {
};
Card.propTypes = {
id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
checked: PropTypes.bool,
children: PropTypes.node,
errorMessage: PropTypes.string,
hasError: PropTypes.bool,
mime: PropTypes.string,
name: PropTypes.string,
onClick: PropTypes.func,
size: PropTypes.number,
small: PropTypes.bool,
type: PropTypes.string,

View File

@ -15,14 +15,22 @@ const Wrapper = styled.div`
cursor: pointer;
font-size: 11px;
color: ${({ color }) => color};
${({ type }) =>
type === 'link' &&
`
transform: rotate(90deg)
`};
`;
Wrapper.defaultProps = {
color: '#b3b5b9',
type: null,
};
Wrapper.propTypes = {
color: PropTypes.string,
type: PropTypes.string,
...themePropTypes,
};

View File

@ -7,7 +7,7 @@ import Wrapper from './Wrapper';
const CardControl = ({ color, onClick, type }) => {
return (
<Wrapper onClick={onClick} color={color}>
<Wrapper onClick={onClick} color={color} type={type}>
{type === 'pencil' && <Pencil fill={color} />}
{type === 'clear' && <ClearIcon fill={color} />}
{!['pencil', 'clear'].includes(type) && <FontAwesomeIcon icon={type} />}

View File

@ -1,10 +1,12 @@
import React, { useState, useEffect, useRef } from 'react';
import { CopyToClipboard } from 'react-copy-to-clipboard';
import { get } from 'lodash';
import PropTypes from 'prop-types';
import { Inputs } from '@buffetjs/custom';
import { useGlobalContext } from 'strapi-helper-plugin';
import Cropper from 'cropperjs';
import 'cropperjs/dist/cropper.css';
import { getTrad, prefixFileUrlWithBackendUrl } from '../../utils';
import CardControl from '../CardControl';
import CardControlsWrapper from '../CardControlsWrapper';
import CardPreview from '../CardPreview';
@ -32,7 +34,9 @@ const EditForm = ({
const [infos, setInfos] = useState({ width: 0, height: 0 });
const [src, setSrc] = useState(null);
const mimeType = get(fileToEdit, ['file', 'type'], '');
const fileURL = get(fileToEdit, ['file', 'url'], null);
const prefixedFileURL = fileURL ? prefixFileUrlWithBackendUrl(fileURL) : null;
const mimeType = get(fileToEdit, ['file', 'type'], null) || get(fileToEdit, ['file', 'mime'], '');
const isImg = isImageType(mimeType);
// TODO
const canCrop = isImg && !mimeType.includes('svg');
@ -42,16 +46,19 @@ const EditForm = ({
useEffect(() => {
if (isImg) {
// TODO: update when editing existing file
const reader = new FileReader();
if (prefixedFileURL) {
setSrc(prefixedFileURL);
} else {
const reader = new FileReader();
reader.onloadend = () => {
setSrc(reader.result);
};
reader.onloadend = () => {
setSrc(reader.result);
};
reader.readAsDataURL(fileToEdit.file);
reader.readAsDataURL(fileToEdit.file);
}
}
}, [isImg, fileToEdit]);
}, [isImg, fileToEdit, prefixedFileURL]);
useEffect(() => {
if (isCropping) {
@ -78,6 +85,7 @@ const EditForm = ({
}, [cropper, isCropping]);
const handleResize = () => {
// 130
const cropBox = cropper.current.getCropBoxData();
const { width, height } = cropBox;
const roundedWidth = Math.round(width);
@ -128,6 +136,10 @@ const EditForm = ({
onClickDeleteFileToUpload(fileToEdit.originalIndex);
};
const handleCopy = () => {
strapi.notification.info(getTrad('notification.link-copied'));
};
const handleSubmit = e => {
e.preventDefault();
@ -145,6 +157,11 @@ const EditForm = ({
{!isCropping ? (
<>
<CardControl color="#9EA7B8" type="trash-alt" onClick={handleClickDelete} />
{fileURL && (
<CopyToClipboard onCopy={handleCopy} text={prefixedFileURL}>
<CardControl color="#9EA7B8" type="link" onClick={handleClickDelete} />
</CopyToClipboard>
)}
{canCrop && (
<CardControl type="crop" color="#9EA7B8" onClick={handleToggleCropMode} />
)}

View File

@ -8,7 +8,7 @@ import Card from '../Card';
import CardControlsWrapper from '../CardControlsWrapper';
import Wrapper from './Wrapper';
const List = ({ data, onChange, selectedItems }) => {
const List = ({ data, onChange, onClickEditFile, selectedItems }) => {
const matrix = createMatrix(data);
return (
@ -22,7 +22,12 @@ const List = ({ data, onChange, selectedItems }) => {
return (
<div className="col-xs-12 col-md-6 col-xl-3" key={id}>
<Card checked={checked} {...item} url={`${strapi.backendURL}${url}`}>
<Card
checked={checked}
{...item}
onClick={onClickEditFile}
url={`${strapi.backendURL}${url}`}
>
<CardControlsWrapper leftAlign className="card-control-wrapper">
<Checkbox name={`${id}`} onChange={onChange} value={checked} />
</CardControlsWrapper>
@ -40,12 +45,14 @@ const List = ({ data, onChange, selectedItems }) => {
List.defaultProps = {
data: [],
onChange: () => {},
onClickEditFile: () => {},
selectedItems: [],
};
List.propTypes = {
data: PropTypes.array,
onChange: PropTypes.func,
onClickEditFile: PropTypes.func,
selectedItems: PropTypes.array,
};

View File

@ -1,5 +1,5 @@
import React, { useReducer, useState, useEffect } from 'react';
import { includes } from 'lodash';
import { includes, toString } from 'lodash';
import { useHistory, useLocation } from 'react-router-dom';
import { Header } from '@buffetjs/custom';
import { useDebounce, useIsMounted } from '@buffetjs/hooks';
@ -14,7 +14,7 @@ import {
useQuery,
} from 'strapi-helper-plugin';
import { getRequestUrl, getTrad } from '../../utils';
import { formatFileForEditing, getRequestUrl, getTrad } from '../../utils';
import Container from '../../components/Container';
import ControlsWrapper from '../../components/ControlsWrapper';
@ -39,6 +39,8 @@ const HomePage = () => {
const query = useQuery();
const [isModalOpen, setIsModalOpen] = useState(false);
const [isPopupOpen, setIsPopupOpen] = useState(false);
const [fileToEdit, setFileToEdit] = useState(null);
const [modalInitialStep, setModalInitialStep] = useState('browse');
const [searchValue, setSearchValue] = useState(query.get('_q') || '');
const { push } = useHistory();
const { search } = useLocation();
@ -171,6 +173,14 @@ const HomePage = () => {
setSearchValue(value);
};
const handleClickEditFile = id => {
const file = formatFileForEditing(data.find(file => toString(file.id) === toString(id)));
setFileToEdit(file);
setModalInitialStep('edit');
handleClickToggleModal();
};
const handleClearSearch = () => {
setSearchValue('');
};
@ -207,6 +217,11 @@ const HomePage = () => {
fetchListData();
};
const handleModalClose = () => {
setModalInitialStep('browse');
setFileToEdit(null);
};
const handleSelectAll = () => {
dispatch({
type: 'TOGGLE_SELECT_ALL',
@ -288,7 +303,12 @@ const HomePage = () => {
</ControlsWrapper>
{dataCount > 0 ? (
<>
<List data={data} onChange={handleChangeCheck} selectedItems={selectedItems} />
<List
data={data}
onChange={handleChangeCheck}
onClickEditFile={handleClickEditFile}
selectedItems={selectedItems}
/>
<PageFooter
context={{ emitEvent: () => {} }}
count={paginationCount}
@ -299,7 +319,13 @@ const HomePage = () => {
) : (
<ListEmpty onClick={handleClickToggleModal} />
)}
<ModalStepper isOpen={isModalOpen} onToggle={handleClickToggleModal} />
<ModalStepper
initialFileToEdit={fileToEdit}
initialStep={modalInitialStep}
isOpen={isModalOpen}
onClosed={handleModalClose}
onToggle={handleClickToggleModal}
/>
<PopUpWarning
isOpen={isPopupOpen}
toggleModal={handleClickTogglePopup}

View File

@ -11,7 +11,7 @@ import init from './init';
import reducer, { initialState } from './reducer';
import { getTrad } from '../../utils';
const ModalStepper = ({ isOpen, onToggle }) => {
const ModalStepper = ({ initialFileToEdit, initialStep, isOpen, onClosed, onToggle }) => {
const { formatMessage } = useGlobalContext();
const [reducerState, dispatch] = useReducer(reducer, initialState, init);
const { currentStep, fileToEdit, filesToUpload } = reducerState.toJS();
@ -27,6 +27,22 @@ const ModalStepper = ({ isOpen, onToggle }) => {
}
}, [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',
@ -72,6 +88,8 @@ const ModalStepper = ({ isOpen, onToggle }) => {
};
const handleClosed = () => {
onClosed();
dispatch({
type: 'RESET_PROPS',
});
@ -255,11 +273,17 @@ const ModalStepper = ({ isOpen, onToggle }) => {
};
ModalStepper.defaultProps = {
initialFileToEdit: null,
initialStep: 'browse',
onClosed: () => {},
onToggle: () => {},
};
ModalStepper.propTypes = {
initialFileToEdit: PropTypes.object,
initialStep: PropTypes.string,
isOpen: PropTypes.bool.isRequired,
onClosed: PropTypes.func,
onToggle: PropTypes.func,
};

View File

@ -25,6 +25,8 @@ const reducer = (state, action) => {
);
case 'GO_TO':
return state.update('currentStep', () => action.to);
case 'INIT_FILE_TO_EDIT':
return state.update('fileToEdit', () => fromJS(action.fileToEdit));
case 'ON_CHANGE':
return state.updateIn(['fileToEdit', ...action.keys.split('.')], () => action.value);
case 'ON_SUBMIT_EDIT_NEW_FILE': {

View File

@ -29,6 +29,13 @@ const stepper = {
prev: 'upload',
withBackButton: true,
},
edit: {
Component: EditForm,
headers: [getTrad('modal.header.file-detail')],
next: null,
prev: null,
withBackButton: false,
},
};
export default stepper;

View File

@ -24,6 +24,7 @@
"modal.upload-list.sub-header.button": "Add more assets",
"modal.upload-list.footer.button.singular": "Upload {number} asset to the library",
"modal.upload-list.footer.button.plural": "Upload {number} assets to the library",
"notification.link-copied": "Link copied into the clipboard",
"plugin.name": "Media Library",
"plugin.description.long": "Media file management.",
"plugin.description.short": "Media file management.",

View File

@ -0,0 +1,13 @@
import { pick } from 'lodash';
const formatFileForEditing = file => {
return {
file: {
...pick(file, ['size', 'ext', 'width', 'height', 'url', 'mime']),
created_at: file.created_at || file.createdAt,
},
fileInfo: pick(file, ['alternativeText', 'caption', 'name']),
};
};
export default formatFileForEditing;

View File

@ -1,6 +1,8 @@
export { default as createMatrix } from './createMatrix';
export { default as formatBytes } from './formatBytes';
export { default as formatFileForEditing } from './formatFileForEditing';
export { default as getExtension } from './getExtension';
export { default as getRequestUrl } from './getRequestUrl';
export { default as getTrad } from './getTrad';
export { default as getType } from './getType';
export { default as prefixFileUrlWithBackendUrl } from './prefixFileUrlWithBackendUrl';

View File

@ -0,0 +1,5 @@
const prefixFileUrlWithBackendUrl = fileURL => {
return fileURL.startsWith('/') ? `${strapi.backendURL}${fileURL}` : fileURL;
};
export default prefixFileUrlWithBackendUrl;

View File

@ -0,0 +1,87 @@
import formatFileForEditing from '../formatFileForEditing';
describe('UPLOAD | utils | formatFileForEditing', () => {
it('should format a file correctly with bookshelf', () => {
const data = {
size: 22.8,
ext: '.png',
width: 110,
caption: 'test',
previewUrl: null,
height: 110,
created_at: '2020-03-23T11:43:46.729Z',
related: [],
name: 'test',
hash: 'Screenshot_2020-03-09_at_17.52.42.png_edbdfb6446',
url: '/uploads/Screenshot_2020-03-09_at_17.52.42.png_edbdfb6446.png',
provider: 'local',
mime: 'image/png',
updated_at: '2020-03-23T11:43:46.729Z',
alternativeText: 'test',
id: 12,
formats: null,
provider_metadata: null,
};
const expected = {
file: {
size: 22.8,
ext: '.png',
width: 110,
height: 110,
created_at: '2020-03-23T11:43:46.729Z',
url: '/uploads/Screenshot_2020-03-09_at_17.52.42.png_edbdfb6446.png',
mime: 'image/png',
},
fileInfo: {
caption: 'test',
alternativeText: 'test',
name: 'test',
},
};
expect(formatFileForEditing(data)).toEqual(expected);
});
it('should format a file correctly with mongoose', () => {
const data = {
size: 22.8,
ext: '.png',
width: 110,
caption: 'test',
previewUrl: null,
height: 110,
createdAt: '2020-03-23T11:43:46.729Z',
related: [],
name: 'test',
hash: 'Screenshot_2020-03-09_at_17.52.42.png_edbdfb6446',
url: '/uploads/Screenshot_2020-03-09_at_17.52.42.png_edbdfb6446.png',
provider: 'local',
mime: 'image/png',
updated_at: '2020-03-23T11:43:46.729Z',
alternativeText: 'test',
id: 12,
formats: null,
provider_metadata: null,
};
const expected = {
file: {
size: 22.8,
ext: '.png',
width: 110,
height: 110,
created_at: '2020-03-23T11:43:46.729Z',
url: '/uploads/Screenshot_2020-03-09_at_17.52.42.png_edbdfb6446.png',
mime: 'image/png',
},
fileInfo: {
alternativeText: 'test',
caption: 'test',
name: 'test',
},
};
expect(formatFileForEditing(data)).toEqual(expected);
});
});

View File

@ -0,0 +1,17 @@
import prefixFileUrlWithBackendUrl from '../prefixFileUrlWithBackendUrl';
describe('UPLOAD | utils | prefixFileUrlWithBackendUrl', () => {
it("should add the strapi back-end url if the file's url startsWith '/'", () => {
const data = '/upload/test';
const expected = 'http://localhost:1337/upload/test';
expect(prefixFileUrlWithBackendUrl(data)).toEqual(expected);
});
it("should not add the strapi back-end url if the file's url does not start with '/'", () => {
const data = 'test/upload/test';
const expected = 'test/upload/test';
expect(prefixFileUrlWithBackendUrl(data)).toEqual(expected);
});
});

View File

@ -23,6 +23,7 @@ const hoc = () => WrappedComponent => {
};
global.strapi = {
backendURL: 'http://localhost:1337',
injectReducer: hoc,
injectSaga: hoc,
notification: {