Merge pull request #3712 from strapi/ctm/edit-formadata-upgrade

Ctm/edit formadata upgrade
This commit is contained in:
Alexandre BODIN 2019-08-02 09:36:59 +02:00 committed by GitHub
commit 00f0a536a3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 526 additions and 473 deletions

View File

@ -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) => {

View File

@ -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])

View File

@ -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 };

View File

@ -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
);
}); });
}); });

View File

@ -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;
}, {});
}; };

View File

@ -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;
}, {});
};

View File

@ -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;
}