mirror of
https://github.com/strapi/strapi.git
synced 2025-12-29 08:04:51 +00:00
Merge pull request #12714 from strapi/fix/json-string-field-publish
Fix json field not validated on publish
This commit is contained in:
commit
858b0b2080
@ -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
|
||||
|
||||
@ -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);
|
||||
});
|
||||
});
|
||||
@ -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':
|
||||
|
||||
@ -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;
|
||||
@ -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);
|
||||
|
||||
|
||||
@ -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 });
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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;
|
||||
@ -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
|
||||
);
|
||||
}, []);
|
||||
|
||||
// ...
|
||||
};
|
||||
```
|
||||
@ -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);
|
||||
});
|
||||
});
|
||||
@ -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',
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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
|
||||
|
||||
@ -6,7 +6,7 @@ jest.mock('@strapi/helper-plugin', () => ({
|
||||
nonLocalizedFields: { common: 'test' },
|
||||
localizations: ['test'],
|
||||
}),
|
||||
formatComponentData: data => data,
|
||||
formatContentTypeData: data => data,
|
||||
contentManagementUtilRemoveFieldsFromData: data => data,
|
||||
}));
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user