mirror of
https://github.com/strapi/strapi.git
synced 2025-11-08 22:32:02 +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 PropTypes from 'prop-types';
|
||||
import { cloneDeep, get, isEmpty } from 'lodash';
|
||||
import { cloneDeep, get } from 'lodash';
|
||||
import {
|
||||
BackHeader,
|
||||
getQueryParameters,
|
||||
@ -24,7 +24,7 @@ import createYupSchema from './utils/schema';
|
||||
import {
|
||||
getMediaAttributes,
|
||||
cleanData,
|
||||
associateFilesToData,
|
||||
mapDataKeysToFilesToUpload,
|
||||
} from './utils/formatData';
|
||||
|
||||
const getRequestUrl = path => `/${pluginId}/explorer/${path}`;
|
||||
@ -289,121 +289,46 @@ function EditView({
|
||||
emitEvent('willSaveEntry');
|
||||
// Create an object containing all the paths of the media fields
|
||||
const filesMap = getMediaAttributes(layout, groupLayoutsData);
|
||||
// Create the formdata to upload all the files
|
||||
const formDatas = Object.keys(filesMap).reduce((acc, current) => {
|
||||
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({})
|
||||
);
|
||||
// Create an object that maps the keys with the related files to upload
|
||||
const filesToUpload = mapDataKeysToFilesToUpload(filesMap, modifiedData);
|
||||
|
||||
const cleanedData = cleanData(
|
||||
cloneDeep(modifiedData),
|
||||
layout,
|
||||
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 endPoint = isCreatingEntry ? slug : `${slug}/${id}`;
|
||||
|
||||
try {
|
||||
// Time to actually send the data
|
||||
await request(getRequestUrl(endPoint), {
|
||||
await request(
|
||||
getRequestUrl(endPoint),
|
||||
{
|
||||
method,
|
||||
headers,
|
||||
params: { source },
|
||||
body: cleanedDataWithUploadedFiles,
|
||||
body: formData,
|
||||
signal: submitSignal,
|
||||
});
|
||||
},
|
||||
false,
|
||||
false
|
||||
);
|
||||
emitEvent('didSaveEntry');
|
||||
redirectToPreviousPage();
|
||||
} catch (err) {
|
||||
@ -546,7 +471,7 @@ function EditView({
|
||||
}}
|
||||
groupValue={groupValue}
|
||||
key={key}
|
||||
isRepeatable={group.repeatable}
|
||||
isRepeatable={group.repeatable || false}
|
||||
name={name}
|
||||
modifiedData={modifiedData}
|
||||
moveGroupField={(dragIndex, overIndex, name) => {
|
||||
|
||||
@ -65,11 +65,8 @@ function reducer(state, action) {
|
||||
.update('initialData', () => fromJS(action.data))
|
||||
.update('modifiedData', () => fromJS(action.data))
|
||||
.update('isLoading', () => false);
|
||||
case 'GET_GROUP_LAYOUTS_SUCCEEDED':
|
||||
return state
|
||||
.update('groupLayoutsData', () => fromJS(action.groupLayouts))
|
||||
.update('defaultGroupValues', () => fromJS(action.defaultGroupValues))
|
||||
.update('modifiedData', obj => {
|
||||
case 'GET_GROUP_LAYOUTS_SUCCEEDED': {
|
||||
const addTempIdToGroupData = obj => {
|
||||
const { defaultGroupValues } = action;
|
||||
|
||||
if (action.isCreatingEntry === true) {
|
||||
@ -84,13 +81,8 @@ function reducer(state, action) {
|
||||
}, 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) => {
|
||||
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([]));
|
||||
|
||||
@ -100,8 +92,20 @@ function reducer(state, action) {
|
||||
return acc;
|
||||
}, obj);
|
||||
}
|
||||
};
|
||||
|
||||
return state
|
||||
.update('groupLayoutsData', () => fromJS(action.groupLayouts))
|
||||
.update('defaultGroupValues', () => fromJS(action.defaultGroupValues))
|
||||
.update('modifiedData', obj => {
|
||||
return addTempIdToGroupData(obj);
|
||||
})
|
||||
.update('initialData', obj => {
|
||||
return addTempIdToGroupData(obj);
|
||||
})
|
||||
.update('isLoadingForLayouts', () => false);
|
||||
}
|
||||
|
||||
case 'INIT':
|
||||
return initialState
|
||||
.set('initialData', fromJS(action.data))
|
||||
@ -112,11 +116,24 @@ function reducer(state, action) {
|
||||
.delete(action.dragIndex)
|
||||
.insert(action.overIndex, list.get(action.dragIndex));
|
||||
});
|
||||
case 'ON_CHANGE':
|
||||
return state.updateIn(
|
||||
case 'ON_CHANGE': {
|
||||
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],
|
||||
() => action.value
|
||||
);
|
||||
}
|
||||
case 'ON_REMOVE_FIELD':
|
||||
return state
|
||||
.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', () => {
|
||||
let ctLayout;
|
||||
let groupLayouts;
|
||||
describe('Content Manager | EditView | utils | cleanData', () => {
|
||||
let simpleContentTypeLayout;
|
||||
let contentTypeLayout;
|
||||
let grpLayouts;
|
||||
|
||||
beforeEach(() => {
|
||||
ctLayout = {
|
||||
schema: {
|
||||
attributes: {
|
||||
bool: { type: 'boolean' },
|
||||
content: { type: 'richtext' },
|
||||
created_at: { type: 'timestamp' },
|
||||
date: { type: 'date' },
|
||||
enum: { type: 'enumeration', enum: Array(2) },
|
||||
fb_cta: {
|
||||
required: true,
|
||||
type: 'group',
|
||||
group: 'cta_facebook',
|
||||
repeatable: false,
|
||||
simpleContentTypeLayout = simpleCtLayout;
|
||||
contentTypeLayout = ctLayout;
|
||||
grpLayouts = groupLayouts;
|
||||
});
|
||||
|
||||
it('should format de data correctly if the content type has no group and no file has been added', () => {
|
||||
const data = {
|
||||
title: 'test',
|
||||
article: {
|
||||
id: 1,
|
||||
name: 'test',
|
||||
},
|
||||
id: { type: 'integer' },
|
||||
ingredients: {
|
||||
type: 'group',
|
||||
group: 'ingredients',
|
||||
repeatable: true,
|
||||
min: 1,
|
||||
max: 10,
|
||||
articles: [
|
||||
{
|
||||
id: 1,
|
||||
name: 'test',
|
||||
},
|
||||
json: { type: 'json' },
|
||||
linkedTags: {
|
||||
attribute: 'tag',
|
||||
collection: 'tag',
|
||||
column: 'id',
|
||||
isVirtual: true,
|
||||
relationType: 'manyWay',
|
||||
targetModel: 'tag',
|
||||
type: 'relation',
|
||||
{
|
||||
id: 2,
|
||||
name: 'test1',
|
||||
},
|
||||
mainIngredient: {
|
||||
type: 'group',
|
||||
group: 'ingredients',
|
||||
repeatable: false,
|
||||
],
|
||||
picture: {
|
||||
id: 4,
|
||||
url: '/something-test',
|
||||
ext: 'unknown',
|
||||
},
|
||||
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' },
|
||||
|
||||
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 = {
|
||||
cta_facebook: {
|
||||
schema: {
|
||||
attributes: {
|
||||
description: { type: 'text' },
|
||||
id: { type: 'integer' },
|
||||
title: { type: 'string' },
|
||||
expect(cleanData(data, simpleContentTypeLayout, grpLayouts)).toEqual(
|
||||
expected
|
||||
);
|
||||
});
|
||||
|
||||
it('should format the datac correctly when there is a repeatable and a non repeatable field', () => {
|
||||
const data = {
|
||||
bool: 'test',
|
||||
content: 'test',
|
||||
date: null,
|
||||
enum: 'un',
|
||||
fb_cta: {
|
||||
description: 'something cool',
|
||||
title: 'test',
|
||||
},
|
||||
ingredients: [
|
||||
{
|
||||
testMultiple: [
|
||||
{
|
||||
id: 3,
|
||||
url: '/test-test',
|
||||
},
|
||||
new File([''], 'test', { type: 'text/html' }),
|
||||
],
|
||||
test: null,
|
||||
name: 'Super name',
|
||||
},
|
||||
ingredients: {
|
||||
schema: {
|
||||
attributes: {
|
||||
testMultiple: { type: 'media', multiple: true },
|
||||
test: { type: 'media', multiple: false },
|
||||
id: { type: 'integer' },
|
||||
name: { type: 'string' },
|
||||
],
|
||||
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', () => {
|
||||
@ -119,6 +231,8 @@ describe('getMediasAttributes util', () => {
|
||||
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) => {
|
||||
const getMedia = (
|
||||
@ -37,23 +107,7 @@ export const getMediaAttributes = (ctLayout, groupLayouts) => {
|
||||
return getMedia(ctLayout);
|
||||
};
|
||||
|
||||
export const getFilesToUpload = (data, prefix = '') => {
|
||||
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) => {
|
||||
export const helperCleanData = (value, key) => {
|
||||
if (isArray(value)) {
|
||||
return value.map(obj => (obj[key] ? obj[key] : obj));
|
||||
} else if (isObject(value)) {
|
||||
@ -63,95 +117,57 @@ const helperCleanData = (value, key) => {
|
||||
}
|
||||
};
|
||||
|
||||
export const cleanData = (retrievedData, ctLayout, groupLayouts) => {
|
||||
const getType = (schema, attrName) =>
|
||||
get(schema, ['attributes', attrName, 'type'], '');
|
||||
const getOtherInfos = (schema, arr) =>
|
||||
get(schema, ['attributes', ...arr], '');
|
||||
export const mapDataKeysToFilesToUpload = (filesMap, data) => {
|
||||
return Object.keys(filesMap).reduce((acc, current) => {
|
||||
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 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;
|
||||
const getFilesToUpload = path => {
|
||||
const value = get(data, path, []) || [];
|
||||
|
||||
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'])) {
|
||||
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');
|
||||
return value.filter(file => {
|
||||
return file instanceof File;
|
||||
});
|
||||
};
|
||||
const getFileToUpload = path => {
|
||||
const file = get(data, [...path, 0], '');
|
||||
if (file instanceof File) {
|
||||
return [file];
|
||||
}
|
||||
|
||||
acc[current] = cleanedData;
|
||||
return [];
|
||||
};
|
||||
|
||||
if (!isRepeatable) {
|
||||
const currentFilesToUpload = isMultiple
|
||||
? getFilesToUpload(keys)
|
||||
: getFileToUpload([...keys]);
|
||||
|
||||
if (!isEmpty(currentFilesToUpload)) {
|
||||
acc[current] = currentFilesToUpload;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}, {});
|
||||
};
|
||||
|
||||
return recursiveCleanData(retrievedData, ctLayout);
|
||||
};
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
@ -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