diff --git a/packages/core/admin/admin/src/content-manager/components/DynamicTable/CellContent/utils/hasContent.js b/packages/core/admin/admin/src/content-manager/components/DynamicTable/CellContent/utils/hasContent.js index f2b18ddc4e..bec6d453ac 100644 --- a/packages/core/admin/admin/src/content-manager/components/DynamicTable/CellContent/utils/hasContent.js +++ b/packages/core/admin/admin/src/content-manager/components/DynamicTable/CellContent/utils/hasContent.js @@ -1,11 +1,13 @@ import isEmpty from 'lodash/isEmpty'; +import isNumber from 'lodash/isNumber'; import isSingleRelation from './isSingleRelation'; +import isFieldTypeNumber from '../../../../utils/isFieldTypeNumber'; export default function hasContent(type, content, metadatas, fieldSchema) { if (type === 'component') { const { - mainField: { name: mainFieldName }, + mainField: { name: mainFieldName, type: mainFieldType }, } = metadatas; // Repeatable fields show the ID as fallback, in case the mainField @@ -14,7 +16,23 @@ export default function hasContent(type, content, metadatas, fieldSchema) { return content.length > 0; } - return !isEmpty(content[mainFieldName]); + const value = content[mainFieldName]; + + /* The ID field reports itself as type `integer`, which makes it + impossible to distinguish it from other number fields. + + Biginteger fields need to be treated as strings, as `isNumber` + doesn't deal with them. + */ + if ( + isFieldTypeNumber(mainFieldType) && + mainFieldType !== 'biginteger' && + mainFieldName !== 'id' + ) { + return isNumber(value); + } + + return !isEmpty(value); } if (type === 'relation') { @@ -25,5 +43,13 @@ export default function hasContent(type, content, metadatas, fieldSchema) { return content.count > 0; } + /* + Biginteger fields need to be treated as strings, as `isNumber` + doesn't deal with them. + */ + if (isFieldTypeNumber(type) && type !== 'biginteger') { + return isNumber(content); + } + return !isEmpty(content); } diff --git a/packages/core/admin/admin/src/content-manager/components/DynamicTable/CellContent/utils/tests/hasContent.test.js b/packages/core/admin/admin/src/content-manager/components/DynamicTable/CellContent/utils/tests/hasContent.test.js index 0665914ecd..266bb10ec4 100644 --- a/packages/core/admin/admin/src/content-manager/components/DynamicTable/CellContent/utils/tests/hasContent.test.js +++ b/packages/core/admin/admin/src/content-manager/components/DynamicTable/CellContent/utils/tests/hasContent.test.js @@ -1,125 +1,237 @@ import hasContent from '../hasContent'; describe('hasContent', () => { - it('returns true for text content', () => { - const normalizedContent = hasContent('text', 'content'); - expect(normalizedContent).toEqual(true); - }); - - it('returns false for empty text content', () => { - const normalizedContent = hasContent('text', ''); - expect(normalizedContent).toEqual(false); - }); - - it('returns false for undefined text content', () => { - const normalizedContent = hasContent('text', undefined); - expect(normalizedContent).toEqual(false); - }); - - it('extracts content from single components with content', () => { - const normalizedContent = hasContent( - 'component', - { name: 'content', id: 1 }, - { mainField: { name: 'name' } } - ); - expect(normalizedContent).toEqual(true); - }); - - it('extracts content from single components without content', () => { - const normalizedContent = hasContent( - 'component', - { name: '', id: 1 }, - { mainField: { name: 'name' } } - ); - expect(normalizedContent).toEqual(false); - }); - - it('extracts content from repeatable components with content', () => { - const normalizedContent = hasContent( - 'component', - [{ name: 'content_2', value: 'truthy', id: 1 }], - { mainField: { name: 'content_2' } }, - { repeatable: true } - ); - expect(normalizedContent).toEqual(true); - }); - - it('extracts content from repeatable components without content', () => { - const normalizedContent = hasContent( - 'component', - [{ name: 'content_2', value: '', id: 1 }], - { mainField: { name: 'content_2' } }, - { repeatable: true } - ); - expect(normalizedContent).toEqual(true); - }); - - it('extracts content from repeatable components without content', () => { - const normalizedContent = hasContent( - 'component', - [{ id: 1 }, { id: 2 }], - { mainField: { name: 'content_2' } }, - { repeatable: true } - ); - expect(normalizedContent).toEqual(true); - }); - - it('extracts content from repeatable components without content', () => { - const normalizedContent = hasContent( - 'component', - [], - { mainField: { name: 'content_2' } }, - { repeatable: true } - ); - expect(normalizedContent).toEqual(false); - }); - - it('extracts content from multiple relations with content', () => { - const normalizedContent = hasContent('relation', { count: 1 }, undefined, { - relation: 'manyToMany', + describe('number fields', () => { + it('returns true for integer', () => { + const normalizedContent = hasContent('integer', 1); + expect(normalizedContent).toEqual(true); + }); + + it('returns false for string integer', () => { + const normalizedContent = hasContent('integer', '1'); + expect(normalizedContent).toEqual(false); + }); + + it('returns false for undefined text', () => { + const normalizedContent = hasContent('integer', undefined); + expect(normalizedContent).toEqual(false); + }); + + it('returns true for float', () => { + const normalizedContent = hasContent('float', 1.111); + expect(normalizedContent).toEqual(true); + }); + + it('returns true for decimal', () => { + const normalizedContent = hasContent('decimal', 1.111); + expect(normalizedContent).toEqual(true); + }); + + it('returns true for biginteger', () => { + const normalizedContent = hasContent('biginteger', '12345678901234567890'); + expect(normalizedContent).toEqual(true); }); - expect(normalizedContent).toEqual(true); }); - it('extracts content from multiple relations without content', () => { - const normalizedContent = hasContent('relation', { count: 0 }, undefined, { - relation: 'manyToMany', + describe('text', () => { + it('returns true for text content', () => { + const normalizedContent = hasContent('text', 'content'); + expect(normalizedContent).toEqual(true); + }); + + it('returns false for empty text content', () => { + const normalizedContent = hasContent('text', ''); + expect(normalizedContent).toEqual(false); + }); + + it('returns false for undefined text content', () => { + const normalizedContent = hasContent('text', undefined); + expect(normalizedContent).toEqual(false); }); - expect(normalizedContent).toEqual(false); }); - it('extracts content from single relations with content', () => { - const normalizedContent = hasContent('relation', { id: 1 }, undefined, { - relation: 'oneToOne', + describe('single component', () => { + it('extracts content from single components with content', () => { + const normalizedContent = hasContent( + 'component', + { name: 'content', id: 1 }, + { mainField: { name: 'name' } } + ); + expect(normalizedContent).toEqual(true); + }); + + it('extracts content from single components without content', () => { + const normalizedContent = hasContent( + 'component', + { name: '', id: 1 }, + { mainField: { name: 'name' } } + ); + expect(normalizedContent).toEqual(false); + }); + + it('extracts integers from single components with content', () => { + const normalizedContent = hasContent( + 'component', + { number: 1, id: 1 }, + { mainField: { name: 'number', type: 'integer' } } + ); + expect(normalizedContent).toEqual(true); + }); + + it('extracts integers from single components without content', () => { + const normalizedContent = hasContent( + 'component', + { number: null, id: 1 }, + { mainField: { name: 'number', type: 'integer' } } + ); + expect(normalizedContent).toEqual(false); + }); + + it('extracts float from single components with content', () => { + const normalizedContent = hasContent( + 'component', + { number: 1.11, id: 1 }, + { mainField: { name: 'number', type: 'float' } } + ); + expect(normalizedContent).toEqual(true); + }); + + it('extracts float from single components without content', () => { + const normalizedContent = hasContent( + 'component', + { number: null, id: 1 }, + { mainField: { name: 'number', type: 'float' } } + ); + expect(normalizedContent).toEqual(false); + }); + + it('extracts decimal from single components with content', () => { + const normalizedContent = hasContent( + 'component', + { number: 1.11, id: 1 }, + { mainField: { name: 'number', type: 'decimal' } } + ); + expect(normalizedContent).toEqual(true); + }); + + it('extracts decimal from single components without content', () => { + const normalizedContent = hasContent( + 'component', + { number: null, id: 1 }, + { mainField: { name: 'number', type: 'decimal' } } + ); + expect(normalizedContent).toEqual(false); + }); + + it('extracts biginteger from single components with content', () => { + const normalizedContent = hasContent( + 'component', + { number: '12345678901234567890', id: 1 }, + { mainField: { name: 'number', type: 'biginteger' } } + ); + expect(normalizedContent).toEqual(true); + }); + + it('extracts biginteger from single components without content', () => { + const normalizedContent = hasContent( + 'component', + { number: null, id: 1 }, + { mainField: { name: 'number', type: 'biginteger' } } + ); + expect(normalizedContent).toEqual(false); }); - expect(normalizedContent).toEqual(true); }); - it('extracts content from single relations without content', () => { - const normalizedContent = hasContent('relation', null, undefined, { - relation: 'oneToOne', + describe('repeatable components', () => { + it('extracts content from repeatable components with content', () => { + const normalizedContent = hasContent( + 'component', + [{ name: 'content_2', value: 'truthy', id: 1 }], + { mainField: { name: 'content_2' } }, + { repeatable: true } + ); + expect(normalizedContent).toEqual(true); + }); + + it('extracts content from repeatable components without content', () => { + const normalizedContent = hasContent( + 'component', + [{ name: 'content_2', value: '', id: 1 }], + { mainField: { name: 'content_2' } }, + { repeatable: true } + ); + expect(normalizedContent).toEqual(true); + }); + + it('extracts content from repeatable components without content', () => { + const normalizedContent = hasContent( + 'component', + [{ id: 1 }, { id: 2 }], + { mainField: { name: 'content_2' } }, + { repeatable: true } + ); + expect(normalizedContent).toEqual(true); + }); + + it('extracts content from repeatable components without content', () => { + const normalizedContent = hasContent( + 'component', + [], + { mainField: { name: 'content_2' } }, + { repeatable: true } + ); + expect(normalizedContent).toEqual(false); }); - expect(normalizedContent).toEqual(false); }); - it('returns oneToManyMorph relations as false with content', () => { - const normalizedContent = hasContent('relation', { id: 1 }, undefined, { - relation: 'oneToManyMorph', + describe('relations', () => { + it('extracts content from multiple relations with content', () => { + const normalizedContent = hasContent('relation', { count: 1 }, undefined, { + relation: 'manyToMany', + }); + expect(normalizedContent).toEqual(true); }); - expect(normalizedContent).toEqual(false); - }); - it('extracts content from oneToManyMorph relations with content', () => { - const normalizedContent = hasContent('relation', { id: 1 }, undefined, { - relation: 'oneToOneMorph', + it('extracts content from multiple relations without content', () => { + const normalizedContent = hasContent('relation', { count: 0 }, undefined, { + relation: 'manyToMany', + }); + expect(normalizedContent).toEqual(false); }); - expect(normalizedContent).toEqual(true); - }); - it('extracts content from oneToManyMorph relations with content', () => { - const normalizedContent = hasContent('relation', null, undefined, { - relation: 'oneToOneMorph', + it('extracts content from single relations with content', () => { + const normalizedContent = hasContent('relation', { id: 1 }, undefined, { + relation: 'oneToOne', + }); + expect(normalizedContent).toEqual(true); + }); + + it('extracts content from single relations without content', () => { + const normalizedContent = hasContent('relation', null, undefined, { + relation: 'oneToOne', + }); + expect(normalizedContent).toEqual(false); + }); + + it('returns oneToManyMorph relations as false with content', () => { + const normalizedContent = hasContent('relation', { id: 1 }, undefined, { + relation: 'oneToManyMorph', + }); + expect(normalizedContent).toEqual(false); + }); + + it('extracts content from oneToManyMorph relations with content', () => { + const normalizedContent = hasContent('relation', { id: 1 }, undefined, { + relation: 'oneToOneMorph', + }); + expect(normalizedContent).toEqual(true); + }); + + it('extracts content from oneToManyMorph relations with content', () => { + const normalizedContent = hasContent('relation', null, undefined, { + relation: 'oneToOneMorph', + }); + expect(normalizedContent).toEqual(false); }); - expect(normalizedContent).toEqual(false); }); }); diff --git a/packages/core/admin/admin/src/content-manager/components/EditViewDataManagerProvider/utils/schema.js b/packages/core/admin/admin/src/content-manager/components/EditViewDataManagerProvider/utils/schema.js index af9a15def9..db7b94d8dc 100644 --- a/packages/core/admin/admin/src/content-manager/components/EditViewDataManagerProvider/utils/schema.js +++ b/packages/core/admin/admin/src/content-manager/components/EditViewDataManagerProvider/utils/schema.js @@ -7,12 +7,14 @@ import toNumber from 'lodash/toNumber'; import * as yup from 'yup'; import { translatedErrors as errorsTrads } from '@strapi/helper-plugin'; +import isFieldTypeNumber from '../../../utils/isFieldTypeNumber'; + yup.addMethod(yup.mixed, 'defined', function() { - return this.test('defined', errorsTrads.required, value => value !== undefined); + return this.test('defined', errorsTrads.required, (value) => value !== undefined); }); yup.addMethod(yup.array, 'notEmptyMin', function(min) { - return this.test('notEmptyMin', errorsTrads.min, value => { + return this.test('notEmptyMin', errorsTrads.min, (value) => { if (isEmpty(value)) { return true; } @@ -49,7 +51,7 @@ yup.addMethod(yup.string, 'isSuperior', function(message, min) { }); }); -const getAttributes = data => get(data, ['attributes'], {}); +const getAttributes = (data) => get(data, ['attributes'], {}); const createYupSchema = ( model, @@ -95,7 +97,7 @@ const createYupSchema = ( if (attribute.repeatable === true) { const { min, max, required } = attribute; - let componentSchema = yup.lazy(value => { + let componentSchema = yup.lazy((value) => { let baseSchema = yup.array().of(componentFieldSchema); if (min) { @@ -121,7 +123,7 @@ const createYupSchema = ( return acc; } - const componentSchema = yup.lazy(obj => { + const componentSchema = yup.lazy((obj) => { if (obj !== undefined) { return attribute.required === true && !options.isDraft ? componentFieldSchema.defined() @@ -152,7 +154,7 @@ const createYupSchema = ( if (min) { if (attribute.required) { dynamicZoneSchema = dynamicZoneSchema - .test('min', errorsTrads.min, value => { + .test('min', errorsTrads.min, (value) => { if (options.isCreatingEntry) { return value && value.length >= min; } @@ -163,7 +165,7 @@ const createYupSchema = ( return value !== null && value.length >= min; }) - .test('required', errorsTrads.required, value => { + .test('required', errorsTrads.required, (value) => { if (options.isCreatingEntry) { return value !== null || value !== undefined; } @@ -178,7 +180,7 @@ const createYupSchema = ( dynamicZoneSchema = dynamicZoneSchema.notEmptyMin(min); } } else if (attribute.required && !options.isDraft) { - dynamicZoneSchema = dynamicZoneSchema.test('required', errorsTrads.required, value => { + dynamicZoneSchema = dynamicZoneSchema.test('required', errorsTrads.required, (value) => { if (options.isCreatingEntry) { return value !== null || value !== undefined; } @@ -213,7 +215,7 @@ const createYupSchemaAttribute = (type, validations, options) => { if (type === 'json') { schema = yup .mixed(errorsTrads.json) - .test('isJSON', errorsTrads.json, value => { + .test('isJSON', errorsTrads.json, (value) => { if (value === undefined) { return true; } @@ -236,19 +238,19 @@ const createYupSchemaAttribute = (type, validations, options) => { if (['number', 'integer', 'float', 'decimal'].includes(type)) { schema = yup .number() - .transform(cv => (isNaN(cv) ? undefined : cv)) + .transform((cv) => (isNaN(cv) ? undefined : cv)) .typeError(); } - if (['date', 'datetime'].includes(type)) { - schema = yup.date(); - } - if (type === 'biginteger') { schema = yup.string().matches(/^-?\d*$/); } - Object.keys(validations).forEach(validation => { + if (['date', 'datetime'].includes(type)) { + schema = yup.date(); + } + + Object.keys(validations).forEach((validation) => { const validationValue = validations[validation]; if ( @@ -267,13 +269,13 @@ const createYupSchemaAttribute = (type, validations, options) => { if (options.isCreatingEntry) { schema = schema.required(errorsTrads.required); } else { - schema = schema.test('required', errorsTrads.required, value => { + schema = schema.test('required', errorsTrads.required, (value) => { // Field is not touched and the user is editing the entry if (value === undefined && !options.isFromComponent) { return true; } - if (['number', 'integer', 'biginteger', 'float', 'decimal'].includes(type)) { + if (isFieldTypeNumber(type)) { if (value === 0) { return true; } @@ -344,12 +346,12 @@ const createYupSchemaAttribute = (type, validations, options) => { } break; case 'positive': - if (['number', 'integer', 'bigint', 'float', 'decimal'].includes(type)) { + if (isFieldTypeNumber(type)) { schema = schema.positive(); } break; case 'negative': - if (['number', 'integer', 'bigint', 'float', 'decimal'].includes(type)) { + if (isFieldTypeNumber(type)) { schema = schema.negative(); } break; diff --git a/packages/core/admin/admin/src/content-manager/utils/isFieldTypeNumber.js b/packages/core/admin/admin/src/content-manager/utils/isFieldTypeNumber.js new file mode 100644 index 0000000000..bad1fad117 --- /dev/null +++ b/packages/core/admin/admin/src/content-manager/utils/isFieldTypeNumber.js @@ -0,0 +1,3 @@ +export default function isFieldTypeNumber(type) { + return ['integer', 'biginteger', 'decimal', 'float', 'number'].includes(type); +} diff --git a/packages/core/admin/admin/src/content-manager/utils/tests/isFieldTypeNumber.test.js b/packages/core/admin/admin/src/content-manager/utils/tests/isFieldTypeNumber.test.js new file mode 100644 index 0000000000..25b174f063 --- /dev/null +++ b/packages/core/admin/admin/src/content-manager/utils/tests/isFieldTypeNumber.test.js @@ -0,0 +1,18 @@ +import isFieldTypeNumber from '../isFieldTypeNumber'; + +const FIXTURE = [ + ['integer', true], + ['float', true], + ['decimal', true], + ['biginteger', true], + ['number', true], + ['text', false], +]; + +describe('isFieldTypeNumber', () => { + FIXTURE.forEach(([type, expectation]) => { + test(`${type} is ${expectation}`, () => { + expect(isFieldTypeNumber(type)).toBe(expectation); + }); + }); +});