Merge pull request #12714 from strapi/fix/json-string-field-publish

Fix json field not validated on publish
This commit is contained in:
Gustav Hansen 2022-03-10 16:38:16 +01:00 committed by GitHub
commit 858b0b2080
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 150 additions and 89 deletions

View File

@ -6,7 +6,7 @@ import {
useTracking,
useNotification,
useQueryParams,
formatComponentData,
formatContentTypeData,
contentManagementUtilRemoveFieldsFromData,
useGuidedTour,
} from '@strapi/helper-plugin';
@ -88,7 +88,7 @@ const CollectionTypeFormWrapper = ({ allLayoutData, children, slug, id, origin }
allLayoutDataRef.current.components
);
return formatComponentData(
return formatContentTypeData(
cleaned,
allLayoutDataRef.current.contentType,
allLayoutDataRef.current.components
@ -103,7 +103,7 @@ const CollectionTypeFormWrapper = ({ allLayoutData, children, slug, id, origin }
allLayoutData.components
);
acc[current] = formatComponentData(
acc[current] = formatContentTypeData(
defaultComponentForm,
allLayoutData.components[current],
allLayoutData.components
@ -117,7 +117,7 @@ const CollectionTypeFormWrapper = ({ allLayoutData, children, slug, id, origin }
allLayoutData.components
);
const contentTypeDataStructureFormatted = formatComponentData(
const contentTypeDataStructureFormatted = formatContentTypeData(
contentTypeDataStructure,
allLayoutData.contentType,
allLayoutData.components

View File

@ -1,17 +0,0 @@
import isValidJSONString from '../utils/isValidJSONString';
describe('CONTENT MANAGER | COMPONENTS | EditViewDataManagerProvider | isValidJSONString', () => {
it.each([
['"coucou"', true],
['"cou\\" \\"cou"', true],
['"coucou', false],
['"cou" "cou"', false],
['{}', false],
['null', false],
['', false],
['[]', false],
])('%s is a JSON string: %s', (value, expectedResult) => {
const result = isValidJSONString(value);
expect(result).toBe(expectedResult);
});
});

View File

@ -18,12 +18,7 @@ const cleanData = (retrievedData, currentSchema, componentsSchema) => {
switch (attrType) {
case 'json':
try {
cleanedData = JSON.parse(value);
} catch (err) {
cleanedData = value;
}
cleanedData = JSON.parse(value);
break;
// TODO
// case 'date':

View File

@ -1,15 +0,0 @@
const isValidJSONString = value => {
if (typeof value === 'string' && value.startsWith('"') && value.endsWith('"')) {
try {
JSON.parse(value);
return true;
} catch {
return false;
}
}
return false;
};
export default isValidJSONString;

View File

@ -1,8 +1,5 @@
import get from 'lodash/get';
import isBoolean from 'lodash/isBoolean';
import isNumber from 'lodash/isNumber';
import isNull from 'lodash/isNull';
import isObject from 'lodash/isObject';
import isEmpty from 'lodash/isEmpty';
import isNaN from 'lodash/isNaN';
import toNumber from 'lodash/toNumber';
@ -10,8 +7,6 @@ import toNumber from 'lodash/toNumber';
import * as yup from 'yup';
import { translatedErrors as errorsTrads } from '@strapi/helper-plugin';
import isValidJSONString from './isValidJSONString';
yup.addMethod(yup.mixed, 'defined', function() {
return this.test('defined', errorsTrads.required, value => value !== undefined);
});
@ -223,10 +218,6 @@ const createYupSchemaAttribute = (type, validations, options) => {
return true;
}
if (isValidJSONString(value) || isNumber(value) || isNull(value) || isObject(value)) {
return true;
}
try {
JSON.parse(value);

View File

@ -16,7 +16,6 @@ import Label from './Label';
import FieldWrapper from './FieldWrapper';
const WAIT = 600;
const stringify = JSON.stringify;
const DEFAULT_THEME = 'blackboard';
const loadCss = async () => {
@ -80,9 +79,7 @@ class InputJSON extends React.Component {
try {
if (value === null) return this.codeMirror.setValue('');
const nextValue = stringify(value, null, 2);
return this.codeMirror.setValue(nextValue);
return this.codeMirror.setValue(value);
} catch (err) {
return this.setState({ error: true });
}

View File

@ -3,7 +3,7 @@ import { useHistory } from 'react-router-dom';
import get from 'lodash/get';
import {
useTracking,
formatComponentData,
formatContentTypeData,
useQueryParams,
useNotification,
useGuidedTour,
@ -55,7 +55,7 @@ const SingleTypeFormWrapper = ({ allLayoutData, children, slug }) => {
);
// This is needed in order to add a unique id for the repeatable components, in order to make the reorder easier
return formatComponentData(cleaned, allLayoutData.contentType, allLayoutData.components);
return formatContentTypeData(cleaned, allLayoutData.contentType, allLayoutData.components);
},
[allLayoutData]
);
@ -73,7 +73,7 @@ const SingleTypeFormWrapper = ({ allLayoutData, children, slug }) => {
allLayoutData.components
);
acc[current] = formatComponentData(
acc[current] = formatContentTypeData(
defaultComponentForm,
allLayoutData.components[current],
allLayoutData.components
@ -86,7 +86,7 @@ const SingleTypeFormWrapper = ({ allLayoutData, children, slug }) => {
allLayoutData.contentType.attributes,
allLayoutData.components
);
const contentTypeDataStructureFormatted = formatComponentData(
const contentTypeDataStructureFormatted = formatContentTypeData(
contentTypeDataStructure,
allLayoutData.contentType,
allLayoutData.components

View File

@ -4,7 +4,7 @@
import get from 'lodash/get';
import { getType, getOtherInfos } from './getAttributeInfos';
const formatComponentData = (data, ct, composSchema) => {
const formatContentTypeData = (data, ct, composSchema) => {
const recursiveFormatData = (data, schema) => {
return Object.keys(data).reduce((acc, current) => {
const type = getType(schema, current);
@ -18,6 +18,12 @@ const formatComponentData = (data, ct, composSchema) => {
return acc;
}
if (type === 'json') {
acc[current] = JSON.stringify(value, null, 2);
return acc;
}
if (type === 'dynamiczone') {
acc[current] = value.map(componentValue => {
const formattedData = recursiveFormatData(
@ -58,4 +64,4 @@ const formatComponentData = (data, ct, composSchema) => {
return recursiveFormatData(data, ct);
};
export default formatComponentData;
export default formatContentTypeData;

View File

@ -0,0 +1,32 @@
<!--- formatContentTypeData.stories.mdx --->
import { Meta } from '@storybook/addon-docs';
<Meta title="utils/formatContentTypeData" />
# formatContentTypeData
This util is used to format the data received by the backend so it can be used by the admin.
It:
- sets the key `__temp_key__` to each component (easier for reordering repeatable components)
- stringifies JSON fields (easier to harmonize the data format for the json editor from user input or backend data)
## Usage
```js
import { formatContentTypeData } from '@strapi/helper-plugin';
const Compo = ({ allLayoutData }) => {
const allLayoutDataRef = useRef(allLayoutData);
const cleanReceivedData = useCallback(data => {
return formatContentTypeData(
data,
allLayoutDataRef.current.contentType,
allLayoutDataRef.current.components
);
}, []);
// ...
};
```

View File

@ -1,14 +1,19 @@
import formatComponentData from '../formatComponentData';
import formatContentTypeData from '../formatContentTypeData';
import testData from './testData';
const { contentType, components, modifiedData } = testData;
describe('STRAPI_HELPER_PLUGIN | utils | formatComponentData', () => {
describe('STRAPI_HELPER_PLUGIN | utils | formatContentTypeData', () => {
it('should add the __temp_key__ property to each repeatable component object', () => {
const expected = {
createdAt: '2020-04-28T13:22:13.033Z',
dz: [
{ __component: 'compos.sub-compo', id: 7, name: 'name', password: 'password' },
{
__component: 'compos.sub-compo',
id: 7,
name: 'name',
password: 'password',
},
{
id: 4,
name: 'name',
@ -69,6 +74,70 @@ describe('STRAPI_HELPER_PLUGIN | utils | formatComponentData', () => {
updatedAt: '2020-04-28T13:22:13.033Z',
};
expect(formatComponentData(modifiedData, contentType, components)).toEqual(expected);
expect(formatContentTypeData(modifiedData, contentType, components)).toEqual(expected);
});
it('should stringify json fields', () => {
const contentType = {
uid: 'api::test.test',
apiID: 'test',
attributes: {
id: { type: 'integer' },
name: { type: 'string' },
dz: { type: 'dynamiczone', components: ['compos.sub-compo'] },
jsonString: { type: 'json' },
jsonObject: { type: 'json' },
},
};
const components = {
'compos.sub-compo': {
uid: 'compos.sub-compo',
category: 'compos',
attributes: {
id: { type: 'integer' },
name: { type: 'string' },
password: { type: 'password' },
jsonString: { type: 'json' },
jsonObject: { type: 'json' },
},
},
};
const data = {
id: 1,
name: 'name',
dz: [
{
__component: 'compos.sub-compo',
id: 7,
name: 'name',
password: 'password',
jsonString: 'hello',
jsonObject: { hello: true },
},
],
jsonString: 'hello',
jsonObject: { hello: true },
};
const expected = {
id: 1,
name: 'name',
dz: [
{
__component: 'compos.sub-compo',
id: 7,
name: 'name',
password: 'password',
jsonString: '"hello"',
jsonObject: '{\n "hello": true\n}',
},
],
jsonString: '"hello"',
jsonObject: '{\n "hello": true\n}',
};
expect(formatContentTypeData(data, contentType, components)).toEqual(expected);
});
});

View File

@ -50,7 +50,12 @@ const testData = {
modifiedData: {
createdAt: '2020-04-28T13:22:13.033Z',
dz: [
{ __component: 'compos.sub-compo', id: 7, name: 'name', password: 'password' },
{
__component: 'compos.sub-compo',
id: 7,
name: 'name',
password: 'password',
},
{
id: 4,
name: 'name',
@ -140,7 +145,11 @@ const testData = {
id: 1,
name: 'name',
subcomponotrepeatable: { id: 4, name: 'name' },
subrepeatable: [{ id: 1, name: 'name' }, { id: 2, name: 'name' }, { id: 3, name: 'name' }],
subrepeatable: [
{ id: 1, name: 'name' },
{ id: 2, name: 'name' },
{ id: 3, name: 'name' },
],
},
repeatable: [
{
@ -160,7 +169,11 @@ const testData = {
},
expectedNoFieldsModifiedData: {
dz: [
{ __component: 'compos.sub-compo', name: 'name', password: 'password' },
{
__component: 'compos.sub-compo',
name: 'name',
password: 'password',
},
{
name: 'name',
password: 'password',

View File

@ -2,9 +2,7 @@ import { getType, getOtherInfos } from './content-manager/utils/getAttributeInfo
// Contexts
export { default as AppInfosContext } from './contexts/AppInfosContext';
export {
default as AutoReloadOverlayBockerContext,
} from './contexts/AutoReloadOverlayBockerContext';
export { default as AutoReloadOverlayBockerContext } from './contexts/AutoReloadOverlayBockerContext';
export { default as NotificationsContext } from './contexts/NotificationsContext';
export { default as OverlayBlockerContext } from './contexts/OverlayBlockerContext';
@ -71,31 +69,23 @@ export { default as SortIcon } from './icons/SortIcon';
export { default as RemoveRoundedButton } from './icons/RemoveRoundedButton';
// content-manager
export {
default as ContentManagerEditViewDataManagerContext,
} from './content-manager/contexts/ContentManagerEditViewDataManagerContext';
export {
default as useCMEditViewDataManager,
} from './content-manager/hooks/useCMEditViewDataManager';
export { default as ContentManagerEditViewDataManagerContext } from './content-manager/contexts/ContentManagerEditViewDataManagerContext';
export { default as useCMEditViewDataManager } from './content-manager/hooks/useCMEditViewDataManager';
export { getType };
export { getOtherInfos };
// Utils
export { default as auth } from './utils/auth';
export { default as hasPermissions } from './utils/hasPermissions';
export {
default as prefixFileUrlWithBackendUrl,
} from './utils/prefixFileUrlWithBackendUrl/prefixFileUrlWithBackendUrl';
export { default as prefixFileUrlWithBackendUrl } from './utils/prefixFileUrlWithBackendUrl/prefixFileUrlWithBackendUrl';
export { default as prefixPluginTranslations } from './utils/prefixPluginTranslations';
export { default as pxToRem } from './utils/pxToRem';
export { default as to } from './utils/await-to-js';
export { default as setHexOpacity } from './utils/setHexOpacity';
export { default as translatedErrors } from './utils/translatedErrors';
export { default as formatComponentData } from './content-manager/utils/formatComponentData';
export { default as formatContentTypeData } from './content-manager/utils/formatContentTypeData';
export { findMatchingPermissions } from './utils/hasPermissions';
export {
default as contentManagementUtilRemoveFieldsFromData,
} from './content-manager/utils/contentManagementUtilRemoveFieldsFromData';
export { default as contentManagementUtilRemoveFieldsFromData } from './content-manager/utils/contentManagementUtilRemoveFieldsFromData';
export { default as getFileExtension } from './utils/getFileExtension/getFileExtension';
export * from './utils/stopPropagation';
export { default as difference } from './utils/difference';

View File

@ -1,6 +1,6 @@
import {
contentManagementUtilRemoveFieldsFromData,
formatComponentData,
formatContentTypeData,
} from '@strapi/helper-plugin';
import removePasswordAndRelationsFieldFromData from './removePasswordAndRelationsFieldFromData';
@ -22,7 +22,7 @@ const cleanData = (data, { contentType, components }, initialLocalizations) => {
fieldsToRemove
);
return formatComponentData(cleanedClonedData, contentType, components);
return formatContentTypeData(cleanedClonedData, contentType, components);
};
export default cleanData;

View File

@ -4,7 +4,7 @@ import cloneDeep from 'lodash/cloneDeep';
import { parse } from 'qs';
import {
request,
formatComponentData,
formatContentTypeData,
contentManagementUtilRemoveFieldsFromData,
} from '@strapi/helper-plugin';
import pluginId from '../pluginId';
@ -66,7 +66,7 @@ const addCommonFieldsToInitialDataMiddleware = () => ({ getState, dispatch }) =>
);
cleanedMerged.localizations = localizations;
action.data = formatComponentData(
action.data = formatContentTypeData(
cleanedMerged,
currentLayout.contentType,
currentLayout.components

View File

@ -6,7 +6,7 @@ jest.mock('@strapi/helper-plugin', () => ({
nonLocalizedFields: { common: 'test' },
localizations: ['test'],
}),
formatComponentData: data => data,
formatContentTypeData: data => data,
contentManagementUtilRemoveFieldsFromData: data => data,
}));