mirror of
https://github.com/strapi/strapi.git
synced 2025-11-12 08:08:05 +00:00
Merge pull request #3712 from strapi/ctm/edit-formadata-upgrade
Ctm/edit formadata upgrade
This commit is contained in:
commit
00f0a536a3
@ -1,6 +1,6 @@
|
|||||||
import React, { memo, useEffect, useState, useReducer } from 'react';
|
import React, { memo, useEffect, useState, useReducer } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { cloneDeep, get, isEmpty } from 'lodash';
|
import { cloneDeep, get } from 'lodash';
|
||||||
import {
|
import {
|
||||||
BackHeader,
|
BackHeader,
|
||||||
getQueryParameters,
|
getQueryParameters,
|
||||||
@ -24,7 +24,7 @@ import createYupSchema from './utils/schema';
|
|||||||
import {
|
import {
|
||||||
getMediaAttributes,
|
getMediaAttributes,
|
||||||
cleanData,
|
cleanData,
|
||||||
associateFilesToData,
|
mapDataKeysToFilesToUpload,
|
||||||
} from './utils/formatData';
|
} from './utils/formatData';
|
||||||
|
|
||||||
const getRequestUrl = path => `/${pluginId}/explorer/${path}`;
|
const getRequestUrl = path => `/${pluginId}/explorer/${path}`;
|
||||||
@ -289,121 +289,46 @@ function EditView({
|
|||||||
emitEvent('willSaveEntry');
|
emitEvent('willSaveEntry');
|
||||||
// Create an object containing all the paths of the media fields
|
// Create an object containing all the paths of the media fields
|
||||||
const filesMap = getMediaAttributes(layout, groupLayoutsData);
|
const filesMap = getMediaAttributes(layout, groupLayoutsData);
|
||||||
// Create the formdata to upload all the files
|
// Create an object that maps the keys with the related files to upload
|
||||||
const formDatas = Object.keys(filesMap).reduce((acc, current) => {
|
const filesToUpload = mapDataKeysToFilesToUpload(filesMap, modifiedData);
|
||||||
const keys = current.split('.');
|
|
||||||
const isMultiple = get(filesMap, [current, 'multiple'], false);
|
|
||||||
const isGroup = get(filesMap, [current, 'isGroup'], false);
|
|
||||||
const isRepeatable = get(filesMap, [current, 'repeatable'], false);
|
|
||||||
|
|
||||||
const getFilesToUpload = path => {
|
|
||||||
const value = get(modifiedData, path, []) || [];
|
|
||||||
|
|
||||||
return value.filter(file => {
|
|
||||||
return file instanceof File;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
const getFileToUpload = path => {
|
|
||||||
const file = get(modifiedData, [...path, 0], '');
|
|
||||||
if (file instanceof File) {
|
|
||||||
return [file];
|
|
||||||
}
|
|
||||||
|
|
||||||
return [];
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!isRepeatable) {
|
|
||||||
const currentFilesToUpload = isMultiple
|
|
||||||
? getFilesToUpload(keys)
|
|
||||||
: getFileToUpload([...keys]);
|
|
||||||
|
|
||||||
if (!isEmpty(currentFilesToUpload)) {
|
|
||||||
acc[current] = currentFilesToUpload.reduce((acc2, curr) => {
|
|
||||||
acc2.append('files', curr);
|
|
||||||
|
|
||||||
return acc2;
|
|
||||||
}, new FormData());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isGroup && isRepeatable) {
|
|
||||||
const [key, targetKey] = current.split('.');
|
|
||||||
const groupData = get(modifiedData, [key], []);
|
|
||||||
const groupFiles = groupData.reduce((acc1, current, index) => {
|
|
||||||
const files = isMultiple
|
|
||||||
? getFileToUpload([key, index, targetKey])
|
|
||||||
: getFileToUpload([key, index, targetKey]);
|
|
||||||
|
|
||||||
if (!isEmpty(files)) {
|
|
||||||
const toFormData = files.reduce((acc2, curr) => {
|
|
||||||
acc2.append('files', curr);
|
|
||||||
|
|
||||||
return acc2;
|
|
||||||
}, new FormData());
|
|
||||||
|
|
||||||
acc1[`${key}.${index}.${targetKey}`] = toFormData;
|
|
||||||
}
|
|
||||||
|
|
||||||
return acc1;
|
|
||||||
}, {});
|
|
||||||
|
|
||||||
return { ...acc, ...groupFiles };
|
|
||||||
}
|
|
||||||
|
|
||||||
return acc;
|
|
||||||
}, {});
|
|
||||||
// Change the request helper default headers so we can pass a FormData
|
|
||||||
const headers = { 'X-Forwarded-Host': 'strapi' };
|
|
||||||
// Upload the files
|
|
||||||
const mapUploadedFiles = Object.keys(formDatas).reduce(
|
|
||||||
async (acc, current) => {
|
|
||||||
const collection = await acc;
|
|
||||||
try {
|
|
||||||
const uploadedFiles = await request(
|
|
||||||
'/upload',
|
|
||||||
{
|
|
||||||
method: 'POST',
|
|
||||||
body: formDatas[current],
|
|
||||||
headers,
|
|
||||||
signal: submitSignal,
|
|
||||||
},
|
|
||||||
false,
|
|
||||||
false
|
|
||||||
);
|
|
||||||
|
|
||||||
collection[current] = uploadedFiles;
|
|
||||||
|
|
||||||
return collection;
|
|
||||||
} catch (err) {
|
|
||||||
console.log('upload error', err);
|
|
||||||
strapi.notification.error(`${pluginId}.notification.upload.error`);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Promise.resolve({})
|
|
||||||
);
|
|
||||||
|
|
||||||
const cleanedData = cleanData(
|
const cleanedData = cleanData(
|
||||||
cloneDeep(modifiedData),
|
cloneDeep(modifiedData),
|
||||||
layout,
|
layout,
|
||||||
groupLayoutsData
|
groupLayoutsData
|
||||||
);
|
);
|
||||||
const cleanedDataWithUploadedFiles = associateFilesToData(
|
|
||||||
cleanedData,
|
|
||||||
filesMap,
|
|
||||||
await mapUploadedFiles
|
|
||||||
);
|
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
|
||||||
|
formData.append('data', JSON.stringify(cleanedData));
|
||||||
|
|
||||||
|
Object.keys(filesToUpload).forEach(key => {
|
||||||
|
const files = filesToUpload[key];
|
||||||
|
|
||||||
|
files.forEach(file => {
|
||||||
|
formData.append(`files.${key}`, file);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Change the request helper default headers so we can pass a FormData
|
||||||
|
const headers = { 'X-Forwarded-Host': 'strapi' };
|
||||||
const method = isCreatingEntry ? 'POST' : 'PUT';
|
const method = isCreatingEntry ? 'POST' : 'PUT';
|
||||||
const endPoint = isCreatingEntry ? slug : `${slug}/${id}`;
|
const endPoint = isCreatingEntry ? slug : `${slug}/${id}`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Time to actually send the data
|
// Time to actually send the data
|
||||||
await request(getRequestUrl(endPoint), {
|
await request(
|
||||||
method,
|
getRequestUrl(endPoint),
|
||||||
params: { source },
|
{
|
||||||
body: cleanedDataWithUploadedFiles,
|
method,
|
||||||
signal: submitSignal,
|
headers,
|
||||||
});
|
params: { source },
|
||||||
|
body: formData,
|
||||||
|
signal: submitSignal,
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
false
|
||||||
|
);
|
||||||
emitEvent('didSaveEntry');
|
emitEvent('didSaveEntry');
|
||||||
redirectToPreviousPage();
|
redirectToPreviousPage();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@ -546,7 +471,7 @@ function EditView({
|
|||||||
}}
|
}}
|
||||||
groupValue={groupValue}
|
groupValue={groupValue}
|
||||||
key={key}
|
key={key}
|
||||||
isRepeatable={group.repeatable}
|
isRepeatable={group.repeatable || false}
|
||||||
name={name}
|
name={name}
|
||||||
modifiedData={modifiedData}
|
modifiedData={modifiedData}
|
||||||
moveGroupField={(dragIndex, overIndex, name) => {
|
moveGroupField={(dragIndex, overIndex, name) => {
|
||||||
|
|||||||
@ -65,43 +65,47 @@ function reducer(state, action) {
|
|||||||
.update('initialData', () => fromJS(action.data))
|
.update('initialData', () => fromJS(action.data))
|
||||||
.update('modifiedData', () => fromJS(action.data))
|
.update('modifiedData', () => fromJS(action.data))
|
||||||
.update('isLoading', () => false);
|
.update('isLoading', () => false);
|
||||||
case 'GET_GROUP_LAYOUTS_SUCCEEDED':
|
case 'GET_GROUP_LAYOUTS_SUCCEEDED': {
|
||||||
|
const addTempIdToGroupData = obj => {
|
||||||
|
const { defaultGroupValues } = action;
|
||||||
|
|
||||||
|
if (action.isCreatingEntry === true) {
|
||||||
|
return obj.keySeq().reduce((acc, current) => {
|
||||||
|
if (defaultGroupValues[current]) {
|
||||||
|
return acc.set(
|
||||||
|
current,
|
||||||
|
fromJS(defaultGroupValues[current].toSet)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
}, obj);
|
||||||
|
} else {
|
||||||
|
return obj.keySeq().reduce((acc, current) => {
|
||||||
|
if (defaultGroupValues[current] && List.isList(obj.get(current))) {
|
||||||
|
const formatted = obj.get(current).reduce((acc2, curr, index) => {
|
||||||
|
return acc2.set(index, curr.set('_temp__id', index));
|
||||||
|
}, List([]));
|
||||||
|
|
||||||
|
return acc.set(current, formatted);
|
||||||
|
}
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
}, obj);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return state
|
return state
|
||||||
.update('groupLayoutsData', () => fromJS(action.groupLayouts))
|
.update('groupLayoutsData', () => fromJS(action.groupLayouts))
|
||||||
.update('defaultGroupValues', () => fromJS(action.defaultGroupValues))
|
.update('defaultGroupValues', () => fromJS(action.defaultGroupValues))
|
||||||
.update('modifiedData', obj => {
|
.update('modifiedData', obj => {
|
||||||
const { defaultGroupValues } = action;
|
return addTempIdToGroupData(obj);
|
||||||
|
})
|
||||||
if (action.isCreatingEntry === true) {
|
.update('initialData', obj => {
|
||||||
return obj.keySeq().reduce((acc, current) => {
|
return addTempIdToGroupData(obj);
|
||||||
if (defaultGroupValues[current]) {
|
|
||||||
return acc.set(
|
|
||||||
current,
|
|
||||||
fromJS(defaultGroupValues[current].toSet)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return acc;
|
|
||||||
}, obj);
|
|
||||||
} else {
|
|
||||||
return obj.keySeq().reduce((acc, current) => {
|
|
||||||
if (
|
|
||||||
defaultGroupValues[current] &&
|
|
||||||
List.isList(obj.get(current))
|
|
||||||
) {
|
|
||||||
const formatted = obj
|
|
||||||
.get(current)
|
|
||||||
.reduce((acc2, curr, index) => {
|
|
||||||
return acc2.set(index, curr.set('_temp__id', index));
|
|
||||||
}, List([]));
|
|
||||||
|
|
||||||
return acc.set(current, formatted);
|
|
||||||
}
|
|
||||||
|
|
||||||
return acc;
|
|
||||||
}, obj);
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
.update('isLoadingForLayouts', () => false);
|
.update('isLoadingForLayouts', () => false);
|
||||||
|
}
|
||||||
|
|
||||||
case 'INIT':
|
case 'INIT':
|
||||||
return initialState
|
return initialState
|
||||||
.set('initialData', fromJS(action.data))
|
.set('initialData', fromJS(action.data))
|
||||||
@ -112,11 +116,24 @@ function reducer(state, action) {
|
|||||||
.delete(action.dragIndex)
|
.delete(action.dragIndex)
|
||||||
.insert(action.overIndex, list.get(action.dragIndex));
|
.insert(action.overIndex, list.get(action.dragIndex));
|
||||||
});
|
});
|
||||||
case 'ON_CHANGE':
|
case 'ON_CHANGE': {
|
||||||
return state.updateIn(
|
let newState = state;
|
||||||
|
const [nonRepeatableGroupKey] = action.keys;
|
||||||
|
|
||||||
|
if (
|
||||||
|
action.keys.length === 2 &&
|
||||||
|
state.getIn(['modifiedData', nonRepeatableGroupKey]) === null
|
||||||
|
) {
|
||||||
|
newState = state.updateIn(['modifiedData', nonRepeatableGroupKey], () =>
|
||||||
|
fromJS({})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return newState.updateIn(
|
||||||
['modifiedData', ...action.keys],
|
['modifiedData', ...action.keys],
|
||||||
() => action.value
|
() => action.value
|
||||||
);
|
);
|
||||||
|
}
|
||||||
case 'ON_REMOVE_FIELD':
|
case 'ON_REMOVE_FIELD':
|
||||||
return state
|
return state
|
||||||
.removeIn(['modifiedData', ...action.keys])
|
.removeIn(['modifiedData', ...action.keys])
|
||||||
|
|||||||
@ -0,0 +1,126 @@
|
|||||||
|
const ctLayout = {
|
||||||
|
schema: {
|
||||||
|
attributes: {
|
||||||
|
bool: { type: 'boolean' },
|
||||||
|
content: { type: 'richtext' },
|
||||||
|
created_at: { type: 'timestamp' },
|
||||||
|
date: { type: 'date' },
|
||||||
|
enum: { type: 'enumeration', enum: ['un', 'deux'] },
|
||||||
|
fb_cta: {
|
||||||
|
required: true,
|
||||||
|
type: 'group',
|
||||||
|
group: 'cta_facebook',
|
||||||
|
repeatable: false,
|
||||||
|
},
|
||||||
|
id: { type: 'integer' },
|
||||||
|
ingredients: {
|
||||||
|
type: 'group',
|
||||||
|
group: 'ingredients',
|
||||||
|
repeatable: true,
|
||||||
|
min: 1,
|
||||||
|
max: 10,
|
||||||
|
},
|
||||||
|
json: { type: 'json' },
|
||||||
|
linkedTags: {
|
||||||
|
attribute: 'tag',
|
||||||
|
collection: 'tag',
|
||||||
|
column: 'id',
|
||||||
|
isVirtual: true,
|
||||||
|
relationType: 'manyWay',
|
||||||
|
targetModel: 'tag',
|
||||||
|
type: 'relation',
|
||||||
|
},
|
||||||
|
mainIngredient: {
|
||||||
|
type: 'group',
|
||||||
|
group: 'ingredients',
|
||||||
|
repeatable: false,
|
||||||
|
},
|
||||||
|
mainTag: {
|
||||||
|
model: 'tag',
|
||||||
|
type: 'relation',
|
||||||
|
targetModel: 'tag',
|
||||||
|
relationType: 'oneWay',
|
||||||
|
},
|
||||||
|
manyTags: {
|
||||||
|
attribute: 'tag',
|
||||||
|
collection: 'tag',
|
||||||
|
column: 'id',
|
||||||
|
dominant: true,
|
||||||
|
isVirtual: true,
|
||||||
|
relationType: 'manyToMany',
|
||||||
|
targetModel: 'tag',
|
||||||
|
type: 'relation',
|
||||||
|
via: 'linkedArticles',
|
||||||
|
},
|
||||||
|
number: { type: 'integer' },
|
||||||
|
pic: { type: 'media', multiple: false, required: false },
|
||||||
|
pictures: { type: 'media', multiple: true, required: false },
|
||||||
|
published: { type: 'boolean' },
|
||||||
|
title: {
|
||||||
|
type: 'string',
|
||||||
|
default: 'test',
|
||||||
|
required: true,
|
||||||
|
unique: true,
|
||||||
|
},
|
||||||
|
updated_at: { type: 'timestampUpdate' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const groupLayouts = {
|
||||||
|
cta_facebook: {
|
||||||
|
schema: {
|
||||||
|
attributes: {
|
||||||
|
description: { type: 'text' },
|
||||||
|
id: { type: 'integer' },
|
||||||
|
title: { type: 'string' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ingredients: {
|
||||||
|
schema: {
|
||||||
|
attributes: {
|
||||||
|
testMultiple: { type: 'media', multiple: true },
|
||||||
|
test: { type: 'media', multiple: false },
|
||||||
|
id: { type: 'integer' },
|
||||||
|
name: { type: 'string' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const simpleCtLayout = {
|
||||||
|
uid: 'simple',
|
||||||
|
schema: {
|
||||||
|
attributes: {
|
||||||
|
title: {
|
||||||
|
type: 'string',
|
||||||
|
default: 'test',
|
||||||
|
},
|
||||||
|
article: {
|
||||||
|
type: 'relation',
|
||||||
|
relationType: 'oneToOne',
|
||||||
|
targetModel: 'article',
|
||||||
|
},
|
||||||
|
articles: {
|
||||||
|
type: 'relation',
|
||||||
|
relationType: 'manyToMany',
|
||||||
|
targetModel: 'article',
|
||||||
|
},
|
||||||
|
picture: {
|
||||||
|
type: 'media',
|
||||||
|
multiple: false,
|
||||||
|
},
|
||||||
|
pictures: {
|
||||||
|
type: 'media',
|
||||||
|
multiple: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// We don't need this key for the test
|
||||||
|
layouts: {},
|
||||||
|
// We don't need this key for the test
|
||||||
|
settings: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
export { ctLayout, groupLayouts, simpleCtLayout };
|
||||||
@ -1,100 +1,212 @@
|
|||||||
import { getMediaAttributes } from '../utils/formatData';
|
import {
|
||||||
|
cleanData,
|
||||||
|
getMediaAttributes,
|
||||||
|
helperCleanData,
|
||||||
|
} from '../utils/formatData';
|
||||||
|
import { ctLayout, groupLayouts, simpleCtLayout } from './data';
|
||||||
|
|
||||||
describe('getMediasAttributes util', () => {
|
describe('Content Manager | EditView | utils | cleanData', () => {
|
||||||
let ctLayout;
|
let simpleContentTypeLayout;
|
||||||
let groupLayouts;
|
let contentTypeLayout;
|
||||||
|
let grpLayouts;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
ctLayout = {
|
simpleContentTypeLayout = simpleCtLayout;
|
||||||
schema: {
|
contentTypeLayout = ctLayout;
|
||||||
attributes: {
|
grpLayouts = groupLayouts;
|
||||||
bool: { type: 'boolean' },
|
});
|
||||||
content: { type: 'richtext' },
|
|
||||||
created_at: { type: 'timestamp' },
|
it('should format de data correctly if the content type has no group and no file has been added', () => {
|
||||||
date: { type: 'date' },
|
const data = {
|
||||||
enum: { type: 'enumeration', enum: Array(2) },
|
title: 'test',
|
||||||
fb_cta: {
|
article: {
|
||||||
required: true,
|
id: 1,
|
||||||
type: 'group',
|
name: 'test',
|
||||||
group: 'cta_facebook',
|
|
||||||
repeatable: false,
|
|
||||||
},
|
|
||||||
id: { type: 'integer' },
|
|
||||||
ingredients: {
|
|
||||||
type: 'group',
|
|
||||||
group: 'ingredients',
|
|
||||||
repeatable: true,
|
|
||||||
min: 1,
|
|
||||||
max: 10,
|
|
||||||
},
|
|
||||||
json: { type: 'json' },
|
|
||||||
linkedTags: {
|
|
||||||
attribute: 'tag',
|
|
||||||
collection: 'tag',
|
|
||||||
column: 'id',
|
|
||||||
isVirtual: true,
|
|
||||||
relationType: 'manyWay',
|
|
||||||
targetModel: 'tag',
|
|
||||||
type: 'relation',
|
|
||||||
},
|
|
||||||
mainIngredient: {
|
|
||||||
type: 'group',
|
|
||||||
group: 'ingredients',
|
|
||||||
repeatable: false,
|
|
||||||
},
|
|
||||||
mainTag: {
|
|
||||||
model: 'tag',
|
|
||||||
type: 'relation',
|
|
||||||
targetModel: 'tag',
|
|
||||||
relationType: 'oneWay',
|
|
||||||
},
|
|
||||||
manyTags: {
|
|
||||||
attribute: 'tag',
|
|
||||||
collection: 'tag',
|
|
||||||
column: 'id',
|
|
||||||
dominant: true,
|
|
||||||
isVirtual: true,
|
|
||||||
relationType: 'manyToMany',
|
|
||||||
targetModel: 'tag',
|
|
||||||
type: 'relation',
|
|
||||||
via: 'linkedArticles',
|
|
||||||
},
|
|
||||||
number: { type: 'integer' },
|
|
||||||
pic: { type: 'media', multiple: false, required: false },
|
|
||||||
pictures: { type: 'media', multiple: true, required: false },
|
|
||||||
published: { type: 'boolean' },
|
|
||||||
title: {
|
|
||||||
type: 'string',
|
|
||||||
default: 'soupette',
|
|
||||||
required: true,
|
|
||||||
unique: true,
|
|
||||||
},
|
|
||||||
updated_at: { type: 'timestampUpdate' },
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
|
articles: [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
name: 'test',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
name: 'test1',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
picture: {
|
||||||
|
id: 4,
|
||||||
|
url: '/something-test',
|
||||||
|
ext: 'unknown',
|
||||||
|
},
|
||||||
|
|
||||||
|
pictures: [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
url: '/something',
|
||||||
|
ext: 'jpg',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
url: '/something-else',
|
||||||
|
ext: 'png',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
const expected = {
|
||||||
|
title: 'test',
|
||||||
|
article: 1,
|
||||||
|
articles: [1, 2],
|
||||||
|
picture: 4,
|
||||||
|
pictures: [1, 2],
|
||||||
};
|
};
|
||||||
|
|
||||||
groupLayouts = {
|
expect(cleanData(data, simpleContentTypeLayout, grpLayouts)).toEqual(
|
||||||
cta_facebook: {
|
expected
|
||||||
schema: {
|
);
|
||||||
attributes: {
|
});
|
||||||
description: { type: 'text' },
|
|
||||||
id: { type: 'integer' },
|
it('should format the datac correctly when there is a repeatable and a non repeatable field', () => {
|
||||||
title: { type: 'string' },
|
const data = {
|
||||||
},
|
bool: 'test',
|
||||||
},
|
content: 'test',
|
||||||
|
date: null,
|
||||||
|
enum: 'un',
|
||||||
|
fb_cta: {
|
||||||
|
description: 'something cool',
|
||||||
|
title: 'test',
|
||||||
},
|
},
|
||||||
ingredients: {
|
ingredients: [
|
||||||
schema: {
|
{
|
||||||
attributes: {
|
testMultiple: [
|
||||||
testMultiple: { type: 'media', multiple: true },
|
{
|
||||||
test: { type: 'media', multiple: false },
|
id: 3,
|
||||||
id: { type: 'integer' },
|
url: '/test-test',
|
||||||
name: { type: 'string' },
|
},
|
||||||
},
|
new File([''], 'test', { type: 'text/html' }),
|
||||||
|
],
|
||||||
|
test: null,
|
||||||
|
name: 'Super name',
|
||||||
},
|
},
|
||||||
|
],
|
||||||
|
linkedTags: [
|
||||||
|
{
|
||||||
|
name: 'test',
|
||||||
|
id: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
mainIngredient: {
|
||||||
|
name: 'another name',
|
||||||
},
|
},
|
||||||
|
mainTag: {
|
||||||
|
name: 'test1',
|
||||||
|
id: 2,
|
||||||
|
},
|
||||||
|
manyTags: [
|
||||||
|
{
|
||||||
|
name: 'test4',
|
||||||
|
id: 4,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
number: 1,
|
||||||
|
pic: new File([''], 'test1', { type: 'text/html' }),
|
||||||
|
pictures: [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
url: '/test',
|
||||||
|
},
|
||||||
|
new File([''], 'test2', { type: 'text/html' }),
|
||||||
|
],
|
||||||
|
published: true,
|
||||||
|
title: 'test',
|
||||||
};
|
};
|
||||||
|
const expected = {
|
||||||
|
bool: 'test',
|
||||||
|
content: 'test',
|
||||||
|
date: null,
|
||||||
|
enum: 'un',
|
||||||
|
fb_cta: {
|
||||||
|
description: 'something cool',
|
||||||
|
title: 'test',
|
||||||
|
},
|
||||||
|
ingredients: [
|
||||||
|
{
|
||||||
|
testMultiple: [3],
|
||||||
|
test: null,
|
||||||
|
name: 'Super name',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
linkedTags: [1],
|
||||||
|
mainIngredient: {
|
||||||
|
name: 'another name',
|
||||||
|
},
|
||||||
|
mainTag: 2,
|
||||||
|
manyTags: [4],
|
||||||
|
number: 1,
|
||||||
|
pic: null,
|
||||||
|
pictures: [1],
|
||||||
|
published: true,
|
||||||
|
title: 'test',
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(cleanData(data, contentTypeLayout, groupLayouts)).toEqual(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Content Manager | EditView | utils | helperCleanData', () => {
|
||||||
|
let data;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
data = {
|
||||||
|
test: 'something',
|
||||||
|
object: {
|
||||||
|
id: 1,
|
||||||
|
test: 'test',
|
||||||
|
other: 'test',
|
||||||
|
},
|
||||||
|
array: [
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
test: 'test',
|
||||||
|
other: 'test',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
test: 'test1',
|
||||||
|
other: 'test1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
test: 'test2',
|
||||||
|
other: 'test2',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
other: 'super cool',
|
||||||
|
};
|
||||||
|
});
|
||||||
|
it('should return the value if it is not an object', () => {
|
||||||
|
expect(helperCleanData(data.test, 'id')).toEqual('something');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the id of an object', () => {
|
||||||
|
expect(helperCleanData(data.object, 'id')).toEqual(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return an array with the ids of each elements if an array of objects is given', () => {
|
||||||
|
expect(helperCleanData(data.array, 'id')).toEqual([2, 3, 4]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return an array with the objects if the key does not exist', () => {
|
||||||
|
expect(helperCleanData(data.array, 'something')).toEqual(data.array);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Content Manager | EditView | utils | getMediasAttributes', () => {
|
||||||
|
let contentTypeLayout;
|
||||||
|
let grpLayouts;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
contentTypeLayout = ctLayout;
|
||||||
|
grpLayouts = groupLayouts;
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return an array containing the paths of all the medias attributes', () => {
|
it('should return an array containing the paths of all the medias attributes', () => {
|
||||||
@ -119,6 +231,8 @@ describe('getMediasAttributes util', () => {
|
|||||||
pictures: { multiple: true, isGroup: false, repeatable: false },
|
pictures: { multiple: true, isGroup: false, repeatable: false },
|
||||||
};
|
};
|
||||||
|
|
||||||
expect(getMediaAttributes(ctLayout, groupLayouts)).toMatchObject(expected);
|
expect(getMediaAttributes(contentTypeLayout, grpLayouts)).toMatchObject(
|
||||||
|
expected
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,4 +1,74 @@
|
|||||||
import { cloneDeep, get, isArray, isObject, set, unset } from 'lodash';
|
import { get, isArray, isEmpty, isObject } from 'lodash';
|
||||||
|
|
||||||
|
export const cleanData = (retrievedData, ctLayout, groupLayouts) => {
|
||||||
|
const getType = (schema, attrName) =>
|
||||||
|
get(schema, ['attributes', attrName, 'type'], '');
|
||||||
|
const getOtherInfos = (schema, arr) =>
|
||||||
|
get(schema, ['attributes', ...arr], '');
|
||||||
|
|
||||||
|
const recursiveCleanData = (data, layout) => {
|
||||||
|
return Object.keys(data).reduce((acc, current) => {
|
||||||
|
const attrType = getType(layout.schema, current);
|
||||||
|
const value = get(data, current);
|
||||||
|
const group = getOtherInfos(layout.schema, [current, 'group']);
|
||||||
|
const isRepeatable = getOtherInfos(layout.schema, [
|
||||||
|
current,
|
||||||
|
'repeatable',
|
||||||
|
]);
|
||||||
|
let cleanedData;
|
||||||
|
|
||||||
|
switch (attrType) {
|
||||||
|
case 'json':
|
||||||
|
cleanedData = value;
|
||||||
|
break;
|
||||||
|
case 'date':
|
||||||
|
cleanedData =
|
||||||
|
value && value._isAMomentObject === true
|
||||||
|
? value.toISOString()
|
||||||
|
: value;
|
||||||
|
break;
|
||||||
|
case 'media':
|
||||||
|
if (getOtherInfos(layout.schema, [current, 'multiple']) === true) {
|
||||||
|
cleanedData = value
|
||||||
|
? helperCleanData(
|
||||||
|
value.filter(file => !(file instanceof File)),
|
||||||
|
'id'
|
||||||
|
)
|
||||||
|
: null;
|
||||||
|
} else {
|
||||||
|
cleanedData =
|
||||||
|
get(value, 0) instanceof File ? null : get(value, 'id', null);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'group':
|
||||||
|
if (isRepeatable) {
|
||||||
|
cleanedData = value
|
||||||
|
? value.map(data => {
|
||||||
|
delete data._temp__id;
|
||||||
|
const subCleanedData = recursiveCleanData(
|
||||||
|
data,
|
||||||
|
groupLayouts[group]
|
||||||
|
);
|
||||||
|
|
||||||
|
return subCleanedData;
|
||||||
|
})
|
||||||
|
: value;
|
||||||
|
} else {
|
||||||
|
cleanedData = recursiveCleanData(value, groupLayouts[group]);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
cleanedData = helperCleanData(value, 'id');
|
||||||
|
}
|
||||||
|
|
||||||
|
acc[current] = cleanedData;
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
};
|
||||||
|
|
||||||
|
return recursiveCleanData(retrievedData, ctLayout);
|
||||||
|
};
|
||||||
|
|
||||||
export const getMediaAttributes = (ctLayout, groupLayouts) => {
|
export const getMediaAttributes = (ctLayout, groupLayouts) => {
|
||||||
const getMedia = (
|
const getMedia = (
|
||||||
@ -37,23 +107,7 @@ export const getMediaAttributes = (ctLayout, groupLayouts) => {
|
|||||||
return getMedia(ctLayout);
|
return getMedia(ctLayout);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getFilesToUpload = (data, prefix = '') => {
|
export const helperCleanData = (value, key) => {
|
||||||
return Object.keys(data).reduce((acc, current) => {
|
|
||||||
if (isObject(data[current]) && !isArray(data[current])) {
|
|
||||||
return getFilesToUpload(data[current], current);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (get(data, [current]) instanceof File) {
|
|
||||||
const path = prefix !== '' ? `${prefix}.${current}` : current;
|
|
||||||
|
|
||||||
acc[path] = data[current];
|
|
||||||
}
|
|
||||||
|
|
||||||
return acc;
|
|
||||||
}, {});
|
|
||||||
};
|
|
||||||
|
|
||||||
const helperCleanData = (value, key) => {
|
|
||||||
if (isArray(value)) {
|
if (isArray(value)) {
|
||||||
return value.map(obj => (obj[key] ? obj[key] : obj));
|
return value.map(obj => (obj[key] ? obj[key] : obj));
|
||||||
} else if (isObject(value)) {
|
} else if (isObject(value)) {
|
||||||
@ -63,95 +117,57 @@ const helperCleanData = (value, key) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const cleanData = (retrievedData, ctLayout, groupLayouts) => {
|
export const mapDataKeysToFilesToUpload = (filesMap, data) => {
|
||||||
const getType = (schema, attrName) =>
|
return Object.keys(filesMap).reduce((acc, current) => {
|
||||||
get(schema, ['attributes', attrName, 'type'], '');
|
const keys = current.split('.');
|
||||||
const getOtherInfos = (schema, arr) =>
|
const isMultiple = get(filesMap, [current, 'multiple'], false);
|
||||||
get(schema, ['attributes', ...arr], '');
|
const isGroup = get(filesMap, [current, 'isGroup'], false);
|
||||||
|
const isRepeatable = get(filesMap, [current, 'repeatable'], false);
|
||||||
|
|
||||||
const recursiveCleanData = (data, layout) => {
|
const getFilesToUpload = path => {
|
||||||
return Object.keys(data).reduce((acc, current) => {
|
const value = get(data, path, []) || [];
|
||||||
const attrType = getType(layout.schema, current);
|
|
||||||
const value = get(data, current);
|
|
||||||
const group = getOtherInfos(layout.schema, [current, 'group']);
|
|
||||||
const isRepeatable = getOtherInfos(layout.schema, [
|
|
||||||
current,
|
|
||||||
'repeatable',
|
|
||||||
]);
|
|
||||||
let cleanedData;
|
|
||||||
|
|
||||||
switch (attrType) {
|
return value.filter(file => {
|
||||||
case 'json':
|
return file instanceof File;
|
||||||
cleanedData = value;
|
});
|
||||||
break;
|
};
|
||||||
case 'date':
|
const getFileToUpload = path => {
|
||||||
cleanedData =
|
const file = get(data, [...path, 0], '');
|
||||||
value && value._isAMomentObject === true
|
if (file instanceof File) {
|
||||||
? value.toISOString()
|
return [file];
|
||||||
: value;
|
|
||||||
break;
|
|
||||||
case 'media':
|
|
||||||
if (getOtherInfos(layout.schema, [current, 'multiple'])) {
|
|
||||||
cleanedData = value
|
|
||||||
? value.filter(file => !(file instanceof File))
|
|
||||||
: null;
|
|
||||||
} else {
|
|
||||||
cleanedData =
|
|
||||||
get(value, 0) instanceof File ? '' : get(value, 'id', null);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'group':
|
|
||||||
if (isRepeatable) {
|
|
||||||
cleanedData = value
|
|
||||||
? value.map(data => {
|
|
||||||
delete data._temp__id;
|
|
||||||
const subCleanedData = recursiveCleanData(
|
|
||||||
data,
|
|
||||||
groupLayouts[group]
|
|
||||||
);
|
|
||||||
|
|
||||||
return subCleanedData;
|
|
||||||
})
|
|
||||||
: value;
|
|
||||||
} else {
|
|
||||||
cleanedData = recursiveCleanData(value, groupLayouts[group]);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
cleanedData = helperCleanData(value, 'id');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
acc[current] = cleanedData;
|
return [];
|
||||||
|
};
|
||||||
|
|
||||||
return acc;
|
if (!isRepeatable) {
|
||||||
}, {});
|
const currentFilesToUpload = isMultiple
|
||||||
};
|
? getFilesToUpload(keys)
|
||||||
|
: getFileToUpload([...keys]);
|
||||||
|
|
||||||
return recursiveCleanData(retrievedData, ctLayout);
|
if (!isEmpty(currentFilesToUpload)) {
|
||||||
};
|
acc[current] = currentFilesToUpload;
|
||||||
|
}
|
||||||
export const associateFilesToData = (data, filesMap, uploadedFiles) => {
|
|
||||||
const ret = cloneDeep(data);
|
|
||||||
|
|
||||||
Object.keys(uploadedFiles).forEach(key => {
|
|
||||||
const keys = key.split('.');
|
|
||||||
const filesMapKey =
|
|
||||||
keys.length > 2
|
|
||||||
? [keys[0], keys[2]]
|
|
||||||
: [keys[0], keys[1]].filter(k => !!k);
|
|
||||||
const isMultiple = get(filesMap, [...filesMapKey, 'multiple'], false);
|
|
||||||
const cleanedValue = get(uploadedFiles, key, []).map(v =>
|
|
||||||
helperCleanData(v, 'id')
|
|
||||||
);
|
|
||||||
|
|
||||||
if (isMultiple) {
|
|
||||||
const previousFiles = get(data, key, []);
|
|
||||||
set(ret, key, [...previousFiles, ...cleanedValue]);
|
|
||||||
} else {
|
|
||||||
unset(ret, key);
|
|
||||||
set(ret, key, cleanedValue[0] || null);
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
return ret;
|
if (isGroup && isRepeatable) {
|
||||||
|
const [key, targetKey] = current.split('.');
|
||||||
|
const groupData = get(data, [key], []);
|
||||||
|
const groupFiles = groupData.reduce((acc1, current, index) => {
|
||||||
|
const files = isMultiple
|
||||||
|
? getFileToUpload([key, index, targetKey])
|
||||||
|
: getFileToUpload([key, index, targetKey]);
|
||||||
|
|
||||||
|
if (!isEmpty(files)) {
|
||||||
|
acc1[`${key}.${index}.${targetKey}`] = files;
|
||||||
|
}
|
||||||
|
|
||||||
|
return acc1;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
return { ...acc, ...groupFiles };
|
||||||
|
}
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,15 +0,0 @@
|
|||||||
import { isPlainObject, isFunction } from 'lodash';
|
|
||||||
|
|
||||||
export const bindLayout = function (object) {
|
|
||||||
return Object.keys(object).reduce((acc, current) => {
|
|
||||||
if (isPlainObject(object[current])) {
|
|
||||||
acc[current] = bindLayout.call(this, object[current]);
|
|
||||||
} else if (isFunction(object[current])) {
|
|
||||||
acc[current] = object[current].bind(this);
|
|
||||||
} else {
|
|
||||||
acc[current] = object[current];
|
|
||||||
}
|
|
||||||
|
|
||||||
return acc;
|
|
||||||
}, {});
|
|
||||||
};
|
|
||||||
@ -1,130 +0,0 @@
|
|||||||
import {
|
|
||||||
findIndex,
|
|
||||||
forEach,
|
|
||||||
includes,
|
|
||||||
isArray,
|
|
||||||
isBoolean,
|
|
||||||
isEmpty,
|
|
||||||
isNaN,
|
|
||||||
isNull,
|
|
||||||
isNumber,
|
|
||||||
isObject,
|
|
||||||
isUndefined,
|
|
||||||
map,
|
|
||||||
mapKeys,
|
|
||||||
reject,
|
|
||||||
} from 'lodash';
|
|
||||||
|
|
||||||
/* eslint-disable consistent-return */
|
|
||||||
export function getValidationsFromForm(form, formValidations) {
|
|
||||||
map(form, (value, key) => {
|
|
||||||
// Check if the object
|
|
||||||
if (isObject(value) && !isArray(value)) {
|
|
||||||
forEach(value, subValue => {
|
|
||||||
// Check if it has nestedInputs
|
|
||||||
if (isArray(subValue) && value.type !== 'select') {
|
|
||||||
return getValidationsFromForm(subValue, formValidations);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isArray(value) && value.type !== 'select') {
|
|
||||||
return getValidationsFromForm(form[key], formValidations);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Push the target and the validation
|
|
||||||
if (value.name) {
|
|
||||||
formValidations.push({
|
|
||||||
name: value.name,
|
|
||||||
validations: value.validations,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return formValidations;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function checkFormValidity(formData, formValidations) {
|
|
||||||
const errors = [];
|
|
||||||
|
|
||||||
forEach(formData, (value, key) => {
|
|
||||||
const validationValue =
|
|
||||||
formValidations[findIndex(formValidations, ['name', key])];
|
|
||||||
|
|
||||||
if (!isUndefined(validationValue)) {
|
|
||||||
const inputErrors = validate(value, validationValue.validations);
|
|
||||||
|
|
||||||
if (!isEmpty(inputErrors)) {
|
|
||||||
errors.push({ name: key, errors: inputErrors });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return errors;
|
|
||||||
}
|
|
||||||
|
|
||||||
function validate(value, validations) {
|
|
||||||
let errors = [];
|
|
||||||
// Handle i18n
|
|
||||||
const requiredError = { id: 'content-manager.error.validation.required' };
|
|
||||||
mapKeys(validations, (validationValue, validationKey) => {
|
|
||||||
switch (validationKey) {
|
|
||||||
case 'max':
|
|
||||||
if (parseInt(value, 10) > validationValue) {
|
|
||||||
errors.push({ id: 'content-manager.error.validation.max' });
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'min':
|
|
||||||
if (parseInt(value, 10) < validationValue) {
|
|
||||||
errors.push({ id: 'content-manager.error.validation.min' });
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'maxLength':
|
|
||||||
if (value && value.length > validationValue) {
|
|
||||||
errors.push({ id: 'content-manager.error.validation.maxLength' });
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'minLength':
|
|
||||||
if (value && value.length < validationValue) {
|
|
||||||
errors.push({ id: 'content-manager.error.validation.minLength' });
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'required':
|
|
||||||
if (validationValue === true && value.length === 0) {
|
|
||||||
errors.push({ id: 'content-manager.error.validation.required' });
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'regex':
|
|
||||||
if (!new RegExp(validationValue).test(value)) {
|
|
||||||
errors.push({ id: 'content-manager.error.validation.regex' });
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'type':
|
|
||||||
if (validationValue === 'json') {
|
|
||||||
try {
|
|
||||||
if (
|
|
||||||
isObject(value) ||
|
|
||||||
isBoolean(value) ||
|
|
||||||
isNumber(value) ||
|
|
||||||
isArray(value) ||
|
|
||||||
isNaN(value) ||
|
|
||||||
isNull(value)
|
|
||||||
) {
|
|
||||||
value = JSON.parse(JSON.stringify(value));
|
|
||||||
} else {
|
|
||||||
errors.push({ id: 'content-manager.error.validation.json' });
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
errors.push({ id: 'content-manager.error.validation.json' });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (includes(errors, requiredError)) {
|
|
||||||
errors = reject(errors, error => error !== requiredError);
|
|
||||||
}
|
|
||||||
return errors;
|
|
||||||
}
|
|
||||||
Loading…
x
Reference in New Issue
Block a user