Merge branch 'features/media-lib' of github.com:strapi/strapi into media-lib/wysiwyg

This commit is contained in:
soupette 2020-04-10 14:14:34 +02:00
commit 3553025d59
17 changed files with 335 additions and 167 deletions

View File

@ -20,7 +20,7 @@
"model": "file",
"via": "related",
"allowedTypes": [
"images",
"images",
"files",
"videos"
],

View File

@ -15,6 +15,7 @@ import CardControl from '../CardControl';
const BrowseAssets = () => {
const {
allowedTypes,
count,
files,
goTo,
@ -136,6 +137,7 @@ const BrowseAssets = () => {
onChange={handleFileSelection}
selectedItems={selectedFiles}
onCardClick={handleListCardClick}
allowedTypes={allowedTypes}
smallCards
renderCardControl={renderCardControl}
/>

View File

@ -1,6 +1,7 @@
import React, { memo } from 'react';
import PropTypes from 'prop-types';
import { formatBytes, getExtension, getType } from '../../utils';
import { useGlobalContext } from 'strapi-helper-plugin';
import { formatBytes, getExtension, getType, getTrad } from '../../utils';
import Flex from '../Flex';
import Text from '../Text';
@ -15,6 +16,7 @@ import Wrapper from '../CardWrapper';
const Card = ({
id,
isDisabled,
checked,
children,
errorMessage,
@ -33,15 +35,21 @@ const Card = ({
withFileCaching,
withoutFileInfo,
}) => {
const { formatMessage } = useGlobalContext();
const fileSize = formatBytes(size, 0);
const fileType = mime || type;
const handleClick = () => {
onClick(id);
if (!isDisabled) {
onClick(id);
}
};
return (
<Wrapper onClick={handleClick}>
<Wrapper
title={isDisabled ? formatMessage({ id: getTrad('list.assets.type-not-allowed') }) : null}
onClick={handleClick}
>
<CardImgWrapper checked={checked} small={small}>
<CardPreview
hasError={hasError}
@ -84,6 +92,7 @@ Card.defaultProps = {
children: null,
errorMessage: null,
id: null,
isDisabled: false,
hasError: false,
hasIcon: false,
height: null,
@ -102,6 +111,7 @@ Card.defaultProps = {
Card.propTypes = {
id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
isDisabled: PropTypes.bool,
checked: PropTypes.bool,
children: PropTypes.node,
errorMessage: PropTypes.string,

View File

@ -11,10 +11,12 @@ const Wrapper = styled.div`
`;
Wrapper.defaultProps = {
isDisabled: false,
isDraggable: false,
};
Wrapper.propTypes = {
isDisabled: PropTypes.bool,
isDraggable: PropTypes.bool,
};

View File

@ -8,7 +8,7 @@ import Padded from '../Padded';
const Filters = ({ onChange, onClick, filters }) => {
return (
<>
<FiltersPicker filters={filters} onChange={onChange} />
<FiltersPicker onChange={onChange} />
<Padded left size="sm" />
<FiltersList filters={filters} onClick={onClick} />
</>

View File

@ -12,7 +12,7 @@ import InputWrapper from './InputWrapper';
import FilterButton from './FilterButton';
import FilterInput from './FilterInput';
const FiltersCard = ({ onChange, filters }) => {
const FiltersCard = ({ onChange }) => {
const { plugins } = useGlobalContext();
const timestamps = getFileModelTimestamps(plugins);
const [state, dispatch] = useReducer(reducer, initialState, () => init(initialState, timestamps));
@ -20,9 +20,7 @@ const FiltersCard = ({ onChange, filters }) => {
const type = filtersForm[name].type;
const filtersOptions = getFilterType(type);
const options = ['image', 'video', 'file'].filter(
f => !filters.find(e => e.value === f && e.isDisabled)
);
const options = ['image', 'video', 'file'];
const handleChange = ({ target: { name, value } }) => {
dispatch({
@ -89,12 +87,10 @@ const FiltersCard = ({ onChange, filters }) => {
};
FiltersCard.defaultProps = {
filters: [],
onChange: () => {},
};
FiltersCard.propTypes = {
filters: PropTypes.arrayOf(PropTypes.object),
onChange: PropTypes.func,
};

View File

@ -9,7 +9,7 @@ import Picker from '../Picker';
import formatFilter from './utils/formatFilter';
const FiltersPicker = ({ onChange, filters }) => {
const FiltersPicker = ({ onChange }) => {
const handleChange = ({ target: { value } }) => {
onChange({ target: { name: 'filters', value: formatFilter(value) } });
};
@ -25,7 +25,6 @@ const FiltersPicker = ({ onChange, filters }) => {
)}
renderSectionContent={onToggle => (
<FiltersCard
filters={filters}
onChange={e => {
handleChange(e);
onToggle();
@ -38,12 +37,10 @@ const FiltersPicker = ({ onChange, filters }) => {
};
FiltersPicker.defaultProps = {
filters: [],
onChange: () => {},
};
FiltersPicker.propTypes = {
filters: PropTypes.arrayOf(PropTypes.object),
onChange: PropTypes.func,
};

View File

@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
import { Checkbox } from '@buffetjs/core';
import { get } from 'lodash';
import { prefixFileUrlWithBackendUrl } from 'strapi-helper-plugin';
import { getTrad } from '../../utils';
import { getTrad, getType } from '../../utils';
import Card from '../Card';
import CardControlsWrapper from '../CardControlsWrapper';
import ListWrapper from '../ListWrapper';
@ -12,6 +12,7 @@ import ListCell from './ListCell';
import ListRow from './ListRow';
const List = ({
allowedTypes,
clickable,
data,
onChange,
@ -23,7 +24,7 @@ const List = ({
}) => {
const selectedAssets = selectedItems.length;
const handleClick = e => {
const handleCheckboxClick = e => {
e.stopPropagation();
};
@ -40,12 +41,15 @@ const List = ({
{data.map(item => {
const { id } = item;
const url = get(item, ['formats', 'thumbnail', 'url'], item.url);
const isAllowed =
allowedTypes.length > 0 ? allowedTypes.includes(getType(item.mime)) : true;
const checked = selectedItems.findIndex(file => file.id === id) !== -1;
const fileUrl = prefixFileUrlWithBackendUrl(url);
return (
<ListCell key={id}>
<Card
isDisabled={!isAllowed}
checked={checked}
{...item}
hasIcon={clickable}
@ -55,14 +59,16 @@ const List = ({
>
{(checked || canSelect) && (
<>
<CardControlsWrapper leftAlign className="card-control-wrapper">
<Checkbox
name={`${id}`}
onChange={onChange}
onClick={handleClick}
value={checked}
/>
</CardControlsWrapper>
{isAllowed && (
<CardControlsWrapper leftAlign className="card-control-wrapper">
<Checkbox
name={`${id}`}
onChange={onChange}
onClick={handleCheckboxClick}
value={checked}
/>
</CardControlsWrapper>
)}
{renderCardControl && (
<CardControlsWrapper className="card-control-wrapper">
{renderCardControl(id)}
@ -80,6 +86,7 @@ const List = ({
};
List.defaultProps = {
allowedTypes: [],
clickable: false,
canSelect: true,
data: [],
@ -91,6 +98,7 @@ List.defaultProps = {
};
List.propTypes = {
allowedTypes: PropTypes.array,
clickable: PropTypes.bool,
canSelect: PropTypes.bool,
data: PropTypes.array,

View File

@ -1,5 +1,5 @@
import React, { useReducer, useState, useRef, useEffect } from 'react';
import { includes, isEmpty, toString } from 'lodash';
import { includes, isEmpty, toString, isEqual, intersectionWith } from 'lodash';
import { useHistory, useLocation } from 'react-router-dom';
import { Header } from '@buffetjs/custom';
import { useDebounce } from '@buffetjs/hooks';
@ -183,9 +183,15 @@ const HomePage = () => {
let updatedQueryParams = generateNewSearch({ [name]: value });
if (name === 'filters') {
const filters = [...generateFiltersFromSearch(search), value];
const existingFilters = generateFiltersFromSearch(search);
const canAddFilter = intersectionWith(existingFilters, [value], isEqual).length === 0;
updatedQueryParams = generateNewSearch({ [name]: existingFilters });
updatedQueryParams = generateNewSearch({ [name]: filters });
if (canAddFilter) {
const filters = [...existingFilters, value];
updatedQueryParams = generateNewSearch({ [name]: filters });
}
}
if (name === '_limit') {

View File

@ -20,13 +20,6 @@ const InputModal = ({
step,
}) => {
const singularTypes = allowedTypes.map(type => type.substring(0, type.length - 1));
const typesToDisable = ['video', 'image', 'file'].filter(f => !singularTypes.includes(f));
const nContainsFilters = typesToDisable.map(type => ({
name: 'mime',
filter: '_ncontains',
value: type,
isDisabled: true,
}));
return (
<DndProvider backend={HTML5Backend}>
@ -35,7 +28,6 @@ const InputModal = ({
onClosed={onClosed}
initialFilesToUpload={filesToUpload}
initialFileToEdit={fileToEdit}
initialFilters={nContainsFilters}
isOpen={isOpen}
multiple={multiple}
noNavigation={noNavigation}

View File

@ -12,6 +12,7 @@ import {
createNewFilesToUploadArray,
urlSchema,
getFileModelTimestamps,
formatFilters,
} from '../../utils';
import InputModalStepperContext from '../../contexts/InputModal/InputModalDataManager';
import init from './init';
@ -24,7 +25,6 @@ const InputModalStepperProvider = ({
children,
initialFilesToUpload,
initialFileToEdit,
initialFilters,
isOpen,
multiple,
noNavigation,
@ -39,6 +39,7 @@ const InputModalStepperProvider = ({
const [reducerState, dispatch] = useReducer(reducer, initialState, state =>
init({
...state,
allowedTypes,
currentStep: step,
fileToEdit: initialFileToEdit,
selectedFiles: Array.isArray(selectedFiles) ? selectedFiles : [selectedFiles],
@ -50,7 +51,6 @@ const InputModalStepperProvider = ({
: [],
params: {
...state.params,
filters: initialFilters,
_sort: `${updated_at}:DESC`,
},
})
@ -294,10 +294,16 @@ const InputModalStepperProvider = ({
handleRemoveFileToUpload(fileIndex);
};
const getFilters = () => {
const compactedParams = compactParams(params);
const searchParams = generateSearchFromFilters(compactedParams, ['_limit', '_sort', '_start']);
return formatFilters(searchParams);
};
const fetchMediaLibFilesCount = async () => {
const requestURL = getRequestUrl('files/count');
const compactedParams = compactParams(params);
const paramsToSend = generateSearchFromFilters(compactedParams, ['_limit', '_sort', '_start']);
const paramsToSend = getFilters();
try {
return await request(`${requestURL}?${paramsToSend}`, {
@ -322,8 +328,7 @@ const InputModalStepperProvider = ({
const fetchMediaLibFiles = async () => {
const requestURL = getRequestUrl('files');
const compactedParams = compactParams(params);
const paramsToSend = generateSearchFromFilters(compactedParams);
const paramsToSend = getFilters();
try {
return await request(`${requestURL}?${paramsToSend}`, {
@ -474,7 +479,6 @@ InputModalStepperProvider.propTypes = {
children: PropTypes.node.isRequired,
initialFilesToUpload: PropTypes.object,
initialFileToEdit: PropTypes.object,
initialFilters: PropTypes.arrayOf(PropTypes.object),
isOpen: PropTypes.bool,
multiple: PropTypes.bool.isRequired,
noNavigation: PropTypes.bool,
@ -486,7 +490,6 @@ InputModalStepperProvider.propTypes = {
InputModalStepperProvider.defaultProps = {
initialFileToEdit: null,
initialFilters: [],
initialFilesToUpload: null,
isOpen: false,
noNavigation: false,

View File

@ -1,5 +1,5 @@
import produce from 'immer';
import { intersectionWith, differenceWith, unionWith, set } from 'lodash';
import { intersectionWith, differenceWith, unionWith, set, isEqual } from 'lodash';
import {
createNewFilesToDownloadArray,
@ -30,6 +30,17 @@ const reducer = (state, action) =>
// eslint-disable-next-line consistent-return
produce(state, draftState => {
switch (action.type) {
case 'ADD_FILES_TO_UPLOAD': {
draftState.filesToUpload = [
...draftState.filesToUpload,
...createNewFilesToUploadArray(action.filesToUpload),
].map((fileToUpload, index) => ({
...fileToUpload,
originalIndex: index,
}));
draftState.currentStep = action.nextStep;
break;
}
case 'ADD_URLS_TO_FILES_TO_UPLOAD': {
draftState.filesToUpload = [
...draftState.filesToUpload,
@ -43,12 +54,32 @@ const reducer = (state, action) =>
break;
}
case 'CLEAN_FILES_ERROR': {
draftState.filesToUpload.forEach((fileToUpload, index) => {
draftState.filesToUpload[index] = {
...fileToUpload,
hasError: false,
errorMessage: null,
};
});
break;
}
case 'CLEAR_FILES_TO_UPLOAD_AND_DOWNLOAD': {
draftState.filesToUpload = [];
draftState.filesToDownload = [];
break;
}
case 'EDIT_EXISTING_FILE': {
const index = draftState.selectedFiles.findIndex(
selectedFile => selectedFile.id === action.file.id
);
if (index !== -1) {
draftState.selectedFiles[index] = action.file;
}
break;
}
case 'FILE_DOWNLOADED': {
const index = state.filesToUpload.findIndex(file => file.tempId === action.fileTempId);
@ -60,32 +91,13 @@ const reducer = (state, action) =>
break;
}
case 'ON_CHANGE': {
set(draftState.fileToEdit, action.keys.split('.'), action.value);
break;
}
case 'ON_CHANGE_URLS_TO_DOWNLOAD': {
set(draftState, ['filesToDownload'], action.value);
break;
}
case 'GET_DATA_SUCCEEDED': {
draftState.files = action.files;
draftState.count = action.countData.count;
break;
}
case 'SET_PARAM': {
const { name, value } = action.param;
if (name === 'filters') {
draftState.params.filters.push(value);
break;
}
if (name === '_limit') {
draftState.params._start = 0;
}
draftState.params[name] = value;
case 'GO_TO': {
draftState.currentStep = action.to;
break;
}
case 'MOVE_ASSET': {
@ -97,6 +109,18 @@ const reducer = (state, action) =>
break;
}
case 'ON_ABORT_UPLOAD': {
draftState.fileToEdit.isUploading = false;
break;
}
case 'ON_CHANGE': {
set(draftState.fileToEdit, action.keys.split('.'), action.value);
break;
}
case 'ON_CHANGE_URLS_TO_DOWNLOAD': {
set(draftState, ['filesToDownload'], action.value);
break;
}
case 'ON_FILE_SELECTION': {
const { id } = action;
const stringId = id.toString();
@ -111,71 +135,8 @@ const reducer = (state, action) =>
draftState.selectedFiles.push(fileToStore);
break;
}
case 'TOGGLE_SELECT_ALL': {
const comparator = (first, second) => first.id === second.id;
const isSelected =
intersectionWith(state.selectedFiles, state.files, comparator).length ===
state.files.length;
if (isSelected) {
draftState.selectedFiles = differenceWith(state.selectedFiles, state.files, comparator);
break;
}
draftState.selectedFiles = unionWith(state.selectedFiles, state.files, comparator);
break;
}
case 'SET_FILE_ERROR': {
draftState.filesToUpload.forEach((fileToUpload, index) => {
if (fileToUpload.originalIndex === action.fileIndex) {
draftState.filesToUpload[index] = {
...draftState.filesToUpload[index],
isUploading: false,
hasError: true,
errorMessage: action.errorMessage,
};
}
});
break;
}
case 'REMOVE_FILTER': {
const { filterToRemove } = action;
draftState.params.filters.splice(filterToRemove, 1);
break;
}
case 'GO_TO': {
draftState.currentStep = action.to;
break;
}
case 'RESET_PROPS': {
if (action.defaultSort) {
draftState.params._sort = action.defaultSort;
} else {
return initialState;
}
break;
}
case 'SET_FILES_UPLOADING_STATE': {
draftState.filesToUpload.forEach((fileToUpload, index) => {
draftState.filesToUpload[index] = {
...fileToUpload,
isUploading: true,
hasError: false,
errorMessage: null,
};
});
break;
}
case 'ADD_FILES_TO_UPLOAD': {
draftState.filesToUpload = [
...draftState.filesToUpload,
...createNewFilesToUploadArray(action.filesToUpload),
].map((fileToUpload, index) => ({
...fileToUpload,
originalIndex: index,
}));
draftState.currentStep = action.nextStep;
case 'ON_SUBMIT_EDIT_EXISTING_FILE': {
draftState.fileToEdit.isUploading = true;
break;
}
case 'REMOVE_FILE_TO_UPLOAD': {
@ -195,30 +156,37 @@ const reducer = (state, action) =>
draftState.filesToUpload.splice(index, 1);
break;
}
case 'REMOVE_FILTER': {
const { filterToRemove } = action;
draftState.params.filters.splice(filterToRemove, 1);
break;
}
case 'RESET_PROPS': {
if (action.defaultSort) {
draftState.params._sort = action.defaultSort;
} else {
return initialState;
}
break;
}
case 'SET_CROP_RESULT': {
draftState.fileToEdit.file = action.blob;
break;
}
case 'CLEAN_FILES_ERROR': {
case 'SET_FILE_ERROR': {
draftState.filesToUpload.forEach((fileToUpload, index) => {
draftState.filesToUpload[index] = {
...fileToUpload,
hasError: false,
errorMessage: null,
};
if (fileToUpload.originalIndex === action.fileIndex) {
draftState.filesToUpload[index] = {
...draftState.filesToUpload[index],
isUploading: false,
hasError: true,
errorMessage: action.errorMessage,
};
}
});
break;
}
case 'SET_NEW_FILE_TO_EDIT': {
draftState.fileToEdit = draftState.filesToUpload[action.fileIndex];
break;
}
case 'SET_FILE_TO_EDIT': {
draftState.fileToEdit = formatFileForEditing(
state.files.find(file => file.id.toString() === action.fileId.toString())
);
break;
}
case 'SET_FILE_TO_DOWNLOAD_ERROR': {
const index = state.filesToUpload.findIndex(file => file.tempId === action.fileTempId);
@ -231,20 +199,10 @@ const reducer = (state, action) =>
break;
}
case 'SET_FORM_DISABLED': {
draftState.isFormDisabled = action.isFormDisabled;
break;
}
case 'ON_ABORT_UPLOAD': {
draftState.fileToEdit.isUploading = false;
break;
}
case 'TOGGLE_MODAL_WARNING': {
draftState.isWarningDeleteOpen = !state.isWarningDeleteOpen;
break;
}
case 'ON_SUBMIT_EDIT_EXISTING_FILE': {
draftState.fileToEdit.isUploading = true;
case 'SET_FILE_TO_EDIT': {
draftState.fileToEdit = formatFileForEditing(
state.files.find(file => file.id.toString() === action.fileId.toString())
);
break;
}
case 'SET_FILE_TO_EDIT_ERROR': {
@ -253,14 +211,61 @@ const reducer = (state, action) =>
draftState.fileToEdit.errorMessage = action.errorMessage;
break;
}
case 'EDIT_EXISTING_FILE': {
const index = draftState.selectedFiles.findIndex(
selectedFile => selectedFile.id === action.file.id
);
case 'SET_FILES_UPLOADING_STATE': {
draftState.filesToUpload.forEach((fileToUpload, index) => {
draftState.filesToUpload[index] = {
...fileToUpload,
isUploading: true,
hasError: false,
errorMessage: null,
};
});
break;
}
case 'SET_FORM_DISABLED': {
draftState.isFormDisabled = action.isFormDisabled;
break;
}
case 'SET_NEW_FILE_TO_EDIT': {
draftState.fileToEdit = draftState.filesToUpload[action.fileIndex];
break;
}
case 'SET_PARAM': {
const { name, value } = action.param;
if (index !== -1) {
draftState.selectedFiles[index] = action.file;
if (name === 'filters') {
const canAddFilter =
intersectionWith(state.params.filters, [value], isEqual).length === 0;
if (canAddFilter) {
draftState.params.filters.push(value);
}
break;
}
if (name === '_limit') {
draftState.params._start = 0;
}
draftState.params[name] = value;
break;
}
case 'TOGGLE_MODAL_WARNING': {
draftState.isWarningDeleteOpen = !state.isWarningDeleteOpen;
break;
}
case 'TOGGLE_SELECT_ALL': {
const comparator = (first, second) => first.id === second.id;
const isSelected =
intersectionWith(state.selectedFiles, state.files, comparator).length ===
state.files.length;
if (isSelected) {
draftState.selectedFiles = differenceWith(state.selectedFiles, state.files, comparator);
break;
}
draftState.selectedFiles = unionWith(state.selectedFiles, state.files, comparator);
break;
}

View File

@ -1933,4 +1933,105 @@ describe('UPLOAD | containers | ModalStepper | reducer', () => {
expect(reducer(state, action)).toEqual(expected);
});
});
describe('SET_PARAM', () => {
it('should set the _start param to 0 if the pagination limit changed', () => {
const action = {
type: 'SET_PARAM',
param: {
name: '_limit',
value: 50,
},
};
const state = {
params: {
_start: 10,
},
};
const expected = {
params: {
_start: 0,
_limit: 50,
},
};
expect(reducer(state, action)).toEqual(expected);
});
it('should add filter to params', () => {
const action = {
type: 'SET_PARAM',
param: {
name: 'filters',
value: {
name: 'mime',
filter: '_contains',
value: 'image',
},
},
};
const state = {
params: {
_start: 0,
filters: [],
},
};
const expected = {
params: {
_start: 0,
filters: [
{
name: 'mime',
filter: '_contains',
value: 'image',
},
],
},
};
expect(reducer(state, action)).toEqual(expected);
});
it('should not add filter to params if it is already exist', () => {
const action = {
type: 'SET_PARAM',
param: {
name: 'filters',
value: {
name: 'mime',
filter: '_contains',
value: 'image',
},
},
};
const state = {
params: {
_start: 0,
_limit: 50,
filters: [
{
name: 'mime',
filter: '_contains',
value: 'image',
},
],
},
};
const expected = {
params: {
_start: 0,
_limit: 50,
filters: [
{
name: 'mime',
filter: '_contains',
value: 'image',
},
],
},
};
expect(reducer(state, action)).toEqual(expected);
});
});
});

View File

@ -30,6 +30,7 @@
"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.subtitle": "Add a first one to the list.",
"list.assets.type-not-allowed": "This type of file is not allowed.",
"list.assets.selected.plural": "{number} assets selected",
"list.assets.selected.singular": "{number} asset selected",
"modal.header.browse": "Upload assets",

View File

@ -0,0 +1,23 @@
const formatFilters = params => {
const indexOfFileFilterContains = params.indexOf('mime_contains=file');
const indexOfFileFilterNContains = params.indexOf('mime_ncontains=file');
let paramsToReturn = params;
if (indexOfFileFilterContains !== -1) {
paramsToReturn = paramsToReturn.replace(
'mime_contains=file',
'mime_ncontains=image&mime_ncontains=video'
);
}
if (indexOfFileFilterNContains !== -1) {
paramsToReturn = paramsToReturn.replace(
'mime_ncontains=file',
'mime_contains=image&mime_contains=video'
);
}
return paramsToReturn;
};
export default formatFilters;

View File

@ -6,6 +6,7 @@ export { default as createNewFilesToDownloadArray } from './createNewFilesToDown
export { default as createNewFilesToUploadArray } from './createNewFilesToUploadArray';
export { default as formatBytes } from './formatBytes';
export { default as formatFileForEditing } from './formatFileForEditing';
export { default as formatFilters } from './formatFilters';
export { default as generatePageFromStart } from './generatePageFromStart';
export { default as generateStartFromPage } from './generateStartFromPage';
export { default as getExtension } from './getExtension';

View File

@ -0,0 +1,21 @@
import formatFilters from '../formatFilters';
describe('UPLOAD | utils | formatFilters', () => {
it('should remove the file filter and add image and video filters', () => {
const stringParams = '&mime_ncontains=file&mime_contains=file';
const actual = formatFilters(stringParams);
const expected =
'&mime_contains=image&mime_contains=video&mime_ncontains=image&mime_ncontains=video';
expect(actual).toEqual(expected);
});
it('should return the string params if there is no file filter', () => {
const stringParams = '&mime_contains=image&mime_contains=video';
const actual = formatFilters(stringParams);
const expected = '&mime_contains=image&mime_contains=video';
expect(actual).toEqual(expected);
});
});