From 4de43fd8955bcb8ec2836d0436801d779b0381c6 Mon Sep 17 00:00:00 2001 From: hip3r Date: Tue, 22 Oct 2024 19:34:03 +0200 Subject: [PATCH 1/4] feat: option to remove empty image field from content #7120 #7186 --- packages/decap-cms-core/index.d.ts | 1 + .../src/actions/editorialWorkflow.ts | 2 +- .../decap-cms-core/src/actions/entries.ts | 7 ++-- .../constants/__tests__/configSchema.spec.js | 12 ++++++ .../src/constants/configSchema.js | 1 + .../__tests__/serializeEntryValues.spec.js | 12 ++++-- .../src/lib/serializeEntryValues.js | 41 ++++++++++++++++--- packages/decap-cms-core/src/types/redux.ts | 1 + 8 files changed, 64 insertions(+), 13 deletions(-) diff --git a/packages/decap-cms-core/index.d.ts b/packages/decap-cms-core/index.d.ts index 9eb5e7f9855e..f45f9418ad7c 100644 --- a/packages/decap-cms-core/index.d.ts +++ b/packages/decap-cms-core/index.d.ts @@ -400,6 +400,7 @@ declare module 'decap-cms-core' { slug?: CmsSlug; i18n?: CmsI18nConfig; local_backend?: boolean | CmsLocalBackend; + remove_empty_image_field?: boolean; editor?: { preview?: boolean; }; diff --git a/packages/decap-cms-core/src/actions/editorialWorkflow.ts b/packages/decap-cms-core/src/actions/editorialWorkflow.ts index 231caa75de76..10d0ab40a445 100644 --- a/packages/decap-cms-core/src/actions/editorialWorkflow.ts +++ b/packages/decap-cms-core/src/actions/editorialWorkflow.ts @@ -359,7 +359,7 @@ export function persistUnpublishedEntry(collection: Collection, existingUnpublis entry, }); - const serializedEntry = getSerializedEntry(collection, entry); + const serializedEntry = getSerializedEntry(collection, entry, state.config); const serializedEntryDraft = entryDraft.set('entry', serializedEntry); dispatch(unpublishedEntryPersisting(collection, entry.get('slug'))); diff --git a/packages/decap-cms-core/src/actions/entries.ts b/packages/decap-cms-core/src/actions/entries.ts index d7971b854d06..b04016649f49 100644 --- a/packages/decap-cms-core/src/actions/entries.ts +++ b/packages/decap-cms-core/src/actions/entries.ts @@ -34,6 +34,7 @@ import type { ViewFilter, ViewGroup, Entry, + CmsConfig, } from '../types/redux'; import type { EntryValue } from '../valueObjects/Entry'; import type { Backend } from '../backend'; @@ -856,7 +857,7 @@ export function getMediaAssets({ entry }: { entry: EntryMap }) { return assets; } -export function getSerializedEntry(collection: Collection, entry: Entry) { +export function getSerializedEntry(collection: Collection, entry: Entry, config: CmsConfig) { /** * Serialize the values of any fields with registered serializers, and * update the entry and entryDraft with the serialized values. @@ -865,7 +866,7 @@ export function getSerializedEntry(collection: Collection, entry: Entry) { // eslint-disable-next-line @typescript-eslint/no-explicit-any function serializeData(data: any) { - return serializeValues(data, fields); + return serializeValues(data, fields, config); } const serializedData = serializeData(entry.get('data')); @@ -910,7 +911,7 @@ export function persistEntry(collection: Collection) { entry, }); - const serializedEntry = getSerializedEntry(collection, entry); + const serializedEntry = getSerializedEntry(collection, entry, state.config); const serializedEntryDraft = entryDraft.set('entry', serializedEntry); dispatch(entryPersisting(collection, serializedEntry)); return backend diff --git a/packages/decap-cms-core/src/constants/__tests__/configSchema.spec.js b/packages/decap-cms-core/src/constants/__tests__/configSchema.spec.js index d0cae4d34e03..173427e19a6c 100644 --- a/packages/decap-cms-core/src/constants/__tests__/configSchema.spec.js +++ b/packages/decap-cms-core/src/constants/__tests__/configSchema.spec.js @@ -172,6 +172,18 @@ describe('config', () => { }).not.toThrowError(); }); + it('should throw if remove_empty_image_field is not a boolean', () => { + expect(() => { + validateConfig(merge({}, validConfig, { remove_empty_image_field: 'false' })); + }).toThrowError("'remove_empty_image_field' must be boolean"); + }); + + it('should not throw if remove_empty_image_field is a boolean', () => { + expect(() => { + validateConfig(merge({}, validConfig, { remove_empty_image_field: false })); + }).not.toThrowError(); + }); + it('should throw if collection publish is not a boolean', () => { expect(() => { validateConfig(merge({}, validConfig, { collections: [{ publish: 'false' }] })); diff --git a/packages/decap-cms-core/src/constants/configSchema.js b/packages/decap-cms-core/src/constants/configSchema.js index 5efd2cd4c172..233edf0cfd60 100644 --- a/packages/decap-cms-core/src/constants/configSchema.js +++ b/packages/decap-cms-core/src/constants/configSchema.js @@ -156,6 +156,7 @@ function getConfigSchema() { }, ], }, + remove_empty_image_field: { type: 'boolean' }, locale: { type: 'string', examples: ['en', 'fr', 'de'] }, i18n: i18nRoot, site_url: { type: 'string', examples: ['https://example.com'] }, diff --git a/packages/decap-cms-core/src/lib/__tests__/serializeEntryValues.spec.js b/packages/decap-cms-core/src/lib/__tests__/serializeEntryValues.spec.js index 755ce69894c1..4b2aa66be9c8 100644 --- a/packages/decap-cms-core/src/lib/__tests__/serializeEntryValues.spec.js +++ b/packages/decap-cms-core/src/lib/__tests__/serializeEntryValues.spec.js @@ -2,12 +2,18 @@ import { fromJS } from 'immutable'; import { serializeValues, deserializeValues } from '../serializeEntryValues'; -const values = fromJS({ title: 'New Post', unknown: 'Unknown Field' }); -const fields = fromJS([{ name: 'title', widget: 'string' }]); +const values = fromJS({ title: 'New Post', unknown: 'Unknown Field', removed_image: '' }); +const fields = fromJS([{ name: 'title', widget: 'string' }, { name: 'removed_image', widget: 'image' }]); describe('serializeValues', () => { it('should retain unknown fields', () => { expect(serializeValues(values, fields)).toEqual( + fromJS({ title: 'New Post', unknown: 'Unknown Field', removed_image: '' }), + ); + }); + + it('should remove image field', () => { + expect(serializeValues(values, fields, { remove_empty_image_field: true })).toEqual( fromJS({ title: 'New Post', unknown: 'Unknown Field' }), ); }); @@ -16,7 +22,7 @@ describe('serializeValues', () => { describe('deserializeValues', () => { it('should retain unknown fields', () => { expect(deserializeValues(values, fields)).toEqual( - fromJS({ title: 'New Post', unknown: 'Unknown Field' }), + fromJS({ title: 'New Post', unknown: 'Unknown Field', removed_image: '' }), ); }); }); diff --git a/packages/decap-cms-core/src/lib/serializeEntryValues.js b/packages/decap-cms-core/src/lib/serializeEntryValues.js index 578ffa38aa50..e763e070d08b 100644 --- a/packages/decap-cms-core/src/lib/serializeEntryValues.js +++ b/packages/decap-cms-core/src/lib/serializeEntryValues.js @@ -3,6 +3,7 @@ import { Map, List } from 'immutable'; import { getWidgetValueSerializer } from './registry'; +const FLAG_REMOVE_ENTRY = '!~FLAG_REMOVE_ENTRY~!'; /** * Methods for serializing/deserializing entry field values. Most widgets don't * require this for their values, and those that do can typically serialize/ @@ -21,7 +22,7 @@ import { getWidgetValueSerializer } from './registry'; * registered deserialization handlers run on entry load, and serialization * handlers run on persist. */ -function runSerializer(values, fields, method) { +function runSerializer(values, fields, method, config = {}, isRecursive = false) { /** * Reduce the list of fields to a map where keys are field names and values * are field values, serializing the values of fields whose widgets have @@ -38,13 +39,13 @@ function runSerializer(values, fields, method) { if (nestedFields && List.isList(value)) { return acc.set( fieldName, - value.map(val => runSerializer(val, nestedFields, method)), + value.map(val => runSerializer(val, nestedFields, method, config, true)), ); } // Call recursively for fields within objects if (nestedFields && Map.isMap(value)) { - return acc.set(fieldName, runSerializer(value, nestedFields, method)); + return acc.set(fieldName, runSerializer(value, nestedFields, method, config, true)); } // Run serialization method on value if not null or undefined @@ -52,6 +53,11 @@ function runSerializer(values, fields, method) { return acc.set(fieldName, serializer[method](value)); } + // If widget is image with no value set, flag field for removal + if (config.remove_empty_image_field && !value && field.get('widget') === 'image') { + return acc.set(fieldName, FLAG_REMOVE_ENTRY); + } + // If no serializer is registered for the field's widget, use the field as is if (!isNil(value)) { return acc.set(fieldName, value); @@ -60,14 +66,37 @@ function runSerializer(values, fields, method) { return acc; }, Map()); - //preserve unknown fields value + // preserve unknown fields value serializedData = values.mergeDeep(serializedData); + // Remove only on the top level, otherwise `mergeDeep` will reinsert them. + if (config.remove_empty_image_field && !isRecursive) { + serializedData = serializedData.map(v => removeEntriesRecursive(v)) + .filter(v => v !== FLAG_REMOVE_ENTRY); + } + return serializedData; } -export function serializeValues(values, fields) { - return runSerializer(values, fields, 'serialize'); +function removeEntriesRecursive(entry) { + if (List.isList(entry)) { + return entry.map(v => removeEntriesRecursive(v)).filter(v => v !== FLAG_REMOVE_ENTRY); + } else if (Map.isMap(entry)) { + let updatedEntry = entry; + entry.forEach((v, k) => { + if (Map.isMap(v) || List.isList(v)) { + updatedEntry = updatedEntry.set(k, removeEntriesRecursive(v)); + } else if (v === FLAG_REMOVE_ENTRY) { + updatedEntry = updatedEntry.delete(k); + } + }); + return updatedEntry; + } + return entry; +} + +export function serializeValues(values, fields, config) { + return runSerializer(values, fields, 'serialize', config); } export function deserializeValues(values, fields) { diff --git a/packages/decap-cms-core/src/types/redux.ts b/packages/decap-cms-core/src/types/redux.ts index b69a82311532..3a06535007a0 100644 --- a/packages/decap-cms-core/src/types/redux.ts +++ b/packages/decap-cms-core/src/types/redux.ts @@ -414,6 +414,7 @@ export interface CmsConfig { slug?: CmsSlug; i18n?: CmsI18nConfig; local_backend?: boolean | CmsLocalBackend; + remove_empty_image_field?: boolean; editor?: { preview?: boolean; }; From 9b70e0c2dc9f5c776223961fdd39b5d0b68b11d3 Mon Sep 17 00:00:00 2001 From: hip3r Date: Tue, 22 Oct 2024 19:46:46 +0200 Subject: [PATCH 2/4] fix: lint errors --- .../src/lib/__tests__/serializeEntryValues.spec.js | 5 ++++- packages/decap-cms-core/src/lib/serializeEntryValues.js | 3 ++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/decap-cms-core/src/lib/__tests__/serializeEntryValues.spec.js b/packages/decap-cms-core/src/lib/__tests__/serializeEntryValues.spec.js index 4b2aa66be9c8..147697874d61 100644 --- a/packages/decap-cms-core/src/lib/__tests__/serializeEntryValues.spec.js +++ b/packages/decap-cms-core/src/lib/__tests__/serializeEntryValues.spec.js @@ -3,7 +3,10 @@ import { fromJS } from 'immutable'; import { serializeValues, deserializeValues } from '../serializeEntryValues'; const values = fromJS({ title: 'New Post', unknown: 'Unknown Field', removed_image: '' }); -const fields = fromJS([{ name: 'title', widget: 'string' }, { name: 'removed_image', widget: 'image' }]); +const fields = fromJS([ + { name: 'title', widget: 'string' }, + { name: 'removed_image', widget: 'image' }, +]); describe('serializeValues', () => { it('should retain unknown fields', () => { diff --git a/packages/decap-cms-core/src/lib/serializeEntryValues.js b/packages/decap-cms-core/src/lib/serializeEntryValues.js index e763e070d08b..706d39df1d69 100644 --- a/packages/decap-cms-core/src/lib/serializeEntryValues.js +++ b/packages/decap-cms-core/src/lib/serializeEntryValues.js @@ -71,7 +71,8 @@ function runSerializer(values, fields, method, config = {}, isRecursive = false) // Remove only on the top level, otherwise `mergeDeep` will reinsert them. if (config.remove_empty_image_field && !isRecursive) { - serializedData = serializedData.map(v => removeEntriesRecursive(v)) + serializedData = serializedData + .map(v => removeEntriesRecursive(v)) .filter(v => v !== FLAG_REMOVE_ENTRY); } From a085a380ada0488191a92f75692b28133bf7461e Mon Sep 17 00:00:00 2001 From: hip3r Date: Tue, 29 Oct 2024 17:40:30 +0100 Subject: [PATCH 3/4] refactor: remove images from content without changing values --- .../src/lib/serializeEntryValues.js | 53 +++++++++++-------- 1 file changed, 31 insertions(+), 22 deletions(-) diff --git a/packages/decap-cms-core/src/lib/serializeEntryValues.js b/packages/decap-cms-core/src/lib/serializeEntryValues.js index 706d39df1d69..09e3c8f604c7 100644 --- a/packages/decap-cms-core/src/lib/serializeEntryValues.js +++ b/packages/decap-cms-core/src/lib/serializeEntryValues.js @@ -3,7 +3,8 @@ import { Map, List } from 'immutable'; import { getWidgetValueSerializer } from './registry'; -const FLAG_REMOVE_ENTRY = '!~FLAG_REMOVE_ENTRY~!'; +const _pathsToRemove = new Set(); + /** * Methods for serializing/deserializing entry field values. Most widgets don't * require this for their values, and those that do can typically serialize/ @@ -22,7 +23,7 @@ const FLAG_REMOVE_ENTRY = '!~FLAG_REMOVE_ENTRY~!'; * registered deserialization handlers run on entry load, and serialization * handlers run on persist. */ -function runSerializer(values, fields, method, config = {}, isRecursive = false) { +function runSerializer(values, fields, method, config = {}, isRecursive = false, currentPath = '') { /** * Reduce the list of fields to a map where keys are field names and values * are field values, serializing the values of fields whose widgets have @@ -34,18 +35,21 @@ function runSerializer(values, fields, method, config = {}, isRecursive = false) const value = values.get(fieldName); const serializer = getWidgetValueSerializer(field.get('widget')); const nestedFields = field.get('fields'); + const newPath = currentPath ? `${currentPath}.${fieldName}` : fieldName; // Call recursively for fields within lists if (nestedFields && List.isList(value)) { return acc.set( fieldName, - value.map(val => runSerializer(val, nestedFields, method, config, true)), + value.map((val, index) => + runSerializer(val, nestedFields, method, config, true, `${newPath}.${index}`), + ), ); } // Call recursively for fields within objects if (nestedFields && Map.isMap(value)) { - return acc.set(fieldName, runSerializer(value, nestedFields, method, config, true)); + return acc.set(fieldName, runSerializer(value, nestedFields, method, config, true, newPath)); } // Run serialization method on value if not null or undefined @@ -55,7 +59,7 @@ function runSerializer(values, fields, method, config = {}, isRecursive = false) // If widget is image with no value set, flag field for removal if (config.remove_empty_image_field && !value && field.get('widget') === 'image') { - return acc.set(fieldName, FLAG_REMOVE_ENTRY); + _pathsToRemove.add(newPath); } // If no serializer is registered for the field's widget, use the field as is @@ -71,29 +75,34 @@ function runSerializer(values, fields, method, config = {}, isRecursive = false) // Remove only on the top level, otherwise `mergeDeep` will reinsert them. if (config.remove_empty_image_field && !isRecursive) { - serializedData = serializedData - .map(v => removeEntriesRecursive(v)) - .filter(v => v !== FLAG_REMOVE_ENTRY); + serializedData = removeEntriesByPaths(serializedData, _pathsToRemove); + _pathsToRemove.clear(); } return serializedData; } -function removeEntriesRecursive(entry) { - if (List.isList(entry)) { - return entry.map(v => removeEntriesRecursive(v)).filter(v => v !== FLAG_REMOVE_ENTRY); - } else if (Map.isMap(entry)) { - let updatedEntry = entry; - entry.forEach((v, k) => { - if (Map.isMap(v) || List.isList(v)) { - updatedEntry = updatedEntry.set(k, removeEntriesRecursive(v)); - } else if (v === FLAG_REMOVE_ENTRY) { - updatedEntry = updatedEntry.delete(k); - } - }); - return updatedEntry; +function removeEntriesByPaths(data, paths) { + paths.forEach(path => { + data = removeEntryByPath(data, path.split('.')); + }); + return data; +} + +function removeEntryByPath(data, keys) { + if (keys.length === 1) { + return data.delete(keys[0]); } - return entry; + + const [firstKey, ...restKeys] = keys; + const nestedData = data.get(firstKey); + + if (nestedData) { + const updatedNestedData = removeEntryByPath(nestedData, restKeys); + return data.set(firstKey, updatedNestedData); + } + + return data; } export function serializeValues(values, fields, config) { From 24950b09986747cdf14039330617d67f50f2af62 Mon Sep 17 00:00:00 2001 From: Yan <61414485+yanthomasdev@users.noreply.github.com> Date: Mon, 13 Jul 2026 17:19:10 -0300 Subject: [PATCH 4/4] feat: update to `remove_empty_fields` with direct serialization --- packages/decap-cms-core/index.d.ts | 2 +- .../constants/__tests__/configSchema.spec.js | 16 +++- .../src/constants/configSchema.js | 5 +- .../__tests__/serializeEntryValues.spec.js | 93 ++++++++++++++++++- .../src/lib/serializeEntryValues.js | 60 +++++++----- packages/decap-cms-core/src/types/redux.ts | 2 +- 6 files changed, 143 insertions(+), 35 deletions(-) diff --git a/packages/decap-cms-core/index.d.ts b/packages/decap-cms-core/index.d.ts index 3afb85e22b6b..6501dc3b54ad 100644 --- a/packages/decap-cms-core/index.d.ts +++ b/packages/decap-cms-core/index.d.ts @@ -438,7 +438,7 @@ declare module 'decap-cms-core' { slug?: CmsSlug; i18n?: CmsI18nConfig; local_backend?: boolean | CmsLocalBackend; - remove_empty_image_field?: boolean; + remove_empty_fields?: string[]; editor?: { notes?: boolean; preview?: boolean; diff --git a/packages/decap-cms-core/src/constants/__tests__/configSchema.spec.js b/packages/decap-cms-core/src/constants/__tests__/configSchema.spec.js index 3e6e7228db02..38b0bd69a706 100644 --- a/packages/decap-cms-core/src/constants/__tests__/configSchema.spec.js +++ b/packages/decap-cms-core/src/constants/__tests__/configSchema.spec.js @@ -172,15 +172,21 @@ describe('config', () => { }).not.toThrowError(); }); - it('should throw if remove_empty_image_field is not a boolean', () => { + it('should throw if remove_empty_fields is not an array', () => { expect(() => { - validateConfig(merge({}, validConfig, { remove_empty_image_field: 'false' })); - }).toThrowError("'remove_empty_image_field' must be boolean"); + validateConfig(merge({}, validConfig, { remove_empty_fields: 'image' })); + }).toThrowError("'remove_empty_fields' must be array"); }); - it('should not throw if remove_empty_image_field is a boolean', () => { + it('should throw if remove_empty_fields contains a non-string value', () => { expect(() => { - validateConfig(merge({}, validConfig, { remove_empty_image_field: false })); + validateConfig(merge({}, validConfig, { remove_empty_fields: ['image', false] })); + }).toThrowError("'remove_empty_fields[1]' must be string"); + }); + + it('should not throw if remove_empty_fields is a string array', () => { + expect(() => { + validateConfig(merge({}, validConfig, { remove_empty_fields: ['image', 'string'] })); }).not.toThrowError(); }); diff --git a/packages/decap-cms-core/src/constants/configSchema.js b/packages/decap-cms-core/src/constants/configSchema.js index 87d0e9c9f078..413152061262 100644 --- a/packages/decap-cms-core/src/constants/configSchema.js +++ b/packages/decap-cms-core/src/constants/configSchema.js @@ -155,7 +155,10 @@ function getConfigSchema() { }, ], }, - remove_empty_image_field: { type: 'boolean' }, + remove_empty_fields: { + type: 'array', + items: { type: 'string' }, + }, locale: { type: 'string', examples: ['en', 'fr', 'de'] }, i18n: i18nRoot, site_url: { type: 'string', examples: ['https://example.com'] }, diff --git a/packages/decap-cms-core/src/lib/__tests__/serializeEntryValues.spec.js b/packages/decap-cms-core/src/lib/__tests__/serializeEntryValues.spec.js index 147697874d61..791d9b34ae5f 100644 --- a/packages/decap-cms-core/src/lib/__tests__/serializeEntryValues.spec.js +++ b/packages/decap-cms-core/src/lib/__tests__/serializeEntryValues.spec.js @@ -1,6 +1,7 @@ import { fromJS } from 'immutable'; import { serializeValues, deserializeValues } from '../serializeEntryValues'; +import { registerWidgetValueSerializer } from '../registry'; const values = fromJS({ title: 'New Post', unknown: 'Unknown Field', removed_image: '' }); const fields = fromJS([ @@ -15,10 +16,96 @@ describe('serializeValues', () => { ); }); - it('should remove image field', () => { - expect(serializeValues(values, fields, { remove_empty_image_field: true })).toEqual( - fromJS({ title: 'New Post', unknown: 'Unknown Field' }), + it('should remove empty fields for configured widgets', () => { + const configuredValues = values.merge(fromJS({ removed_title: '', count: 0, featured: false })); + const configuredFields = fields.concat( + fromJS([ + { name: 'removed_title', widget: 'string' }, + { name: 'count', widget: 'number' }, + { name: 'featured', widget: 'boolean' }, + ]), ); + + expect( + serializeValues(configuredValues, configuredFields, { + remove_empty_fields: ['image', 'string', 'number', 'boolean'], + }), + ).toEqual( + fromJS({ + title: 'New Post', + unknown: 'Unknown Field', + count: 0, + featured: false, + }), + ); + }); + + it('should remove nested fields and preserve dots in field names', () => { + const nestedValues = fromJS({ + sections: [{ image: '', 'hero.image': '' }, { image: 'image.jpg' }], + }); + const nestedFields = fromJS([ + { + name: 'sections', + widget: 'list', + fields: [ + { name: 'image', widget: 'image' }, + { name: 'hero.image', widget: 'image' }, + ], + }, + ]); + + expect(serializeValues(nestedValues, nestedFields, { remove_empty_fields: ['image'] })).toEqual( + fromJS({ sections: [{}, { image: 'image.jpg' }] }), + ); + }); + + it('should not retain removal paths after a serializer throws', () => { + registerWidgetValueSerializer('throwing-widget', { + serialize: () => { + throw new Error('Serialization failed'); + }, + }); + const failingFields = fromJS([ + { name: 'removed_image', widget: 'image' }, + { name: 'content', widget: 'throwing-widget' }, + ]); + + expect(() => + serializeValues(fromJS({ removed_image: '', content: 'value' }), failingFields, { + remove_empty_fields: ['image'], + }), + ).toThrow('Serialization failed'); + + expect( + serializeValues( + fromJS({ removed_image: 'image.jpg' }), + fromJS([{ name: 'removed_image', widget: 'image' }]), + { remove_empty_fields: ['image'] }, + ), + ).toEqual(fromJS({ removed_image: 'image.jpg' })); + }); + + it('should retain empty fields for widgets that are not configured', () => { + expect(serializeValues(values, fields, { remove_empty_fields: ['string'] })).toEqual( + fromJS({ title: 'New Post', unknown: 'Unknown Field', removed_image: '' }), + ); + }); + + it('should treat fields without a widget as string fields', () => { + expect( + serializeValues(fromJS({ subtitle: '' }), fromJS([{ name: 'subtitle' }]), { + remove_empty_fields: ['string'], + }), + ).toEqual(fromJS({})); + }); + + it('should remove null fields for configured widgets', () => { + expect( + serializeValues(fromJS({ image: null }), fromJS([{ name: 'image', widget: 'image' }]), { + remove_empty_fields: ['image'], + }), + ).toEqual(fromJS({})); }); }); diff --git a/packages/decap-cms-core/src/lib/serializeEntryValues.js b/packages/decap-cms-core/src/lib/serializeEntryValues.js index 194bb58e3342..c1f2ef8aa890 100644 --- a/packages/decap-cms-core/src/lib/serializeEntryValues.js +++ b/packages/decap-cms-core/src/lib/serializeEntryValues.js @@ -3,8 +3,6 @@ import { Map, List } from 'immutable'; import { getWidgetValueSerializer } from './registry'; -const _pathsToRemove = new Set(); - /** * Methods for serializing/deserializing entry field values. Most widgets don't * require this for their values, and those that do can typically serialize/ @@ -23,33 +21,52 @@ const _pathsToRemove = new Set(); * registered deserialization handlers run on entry load, and serialization * handlers run on persist. */ -function runSerializer(values, fields, method, config = {}, isRecursive = false, currentPath = '') { +function runSerializer( + values, + fields, + method, + removeEmptyFields = [], + pathsToRemove = [], + currentPath = [], +) { /** * Reduce the list of fields to a map where keys are field names and values * are field values, serializing the values of fields whose widgets have * registered serializers. If the field is a list or object, call recursively * for nested fields. */ - let serializedData = fields.reduce((acc, field) => { + const serializedData = fields.reduce((acc, field) => { const fieldName = field.get('name'); const value = values.get(fieldName); - const serializer = getWidgetValueSerializer(field.get('widget')); + const widget = field.get('widget', 'string'); + const serializer = getWidgetValueSerializer(widget); const nestedFields = field.get('fields'); - const newPath = currentPath ? `${currentPath}.${fieldName}` : fieldName; + const newPath = [...currentPath, fieldName]; // Call recursively for fields within lists if (nestedFields && List.isList(value)) { return acc.set( fieldName, value.map((val, index) => - runSerializer(val, nestedFields, method, config, true, `${newPath}.${index}`), + runSerializer(val, nestedFields, method, removeEmptyFields, pathsToRemove, [ + ...newPath, + index, + ]), ), ); } // Call recursively for fields within objects if (nestedFields && Map.isMap(value)) { - return acc.set(fieldName, runSerializer(value, nestedFields, method, config, true, newPath)); + return acc.set( + fieldName, + runSerializer(value, nestedFields, method, removeEmptyFields, pathsToRemove, newPath), + ); + } + + if (removeEmptyFields.includes(widget) && (isNil(value) || value === '')) { + pathsToRemove.push(newPath); + return acc; } // Run serialization method on value if not null or undefined @@ -57,11 +74,6 @@ function runSerializer(values, fields, method, config = {}, isRecursive = false, return acc.set(fieldName, serializer[method](value)); } - // If widget is image with no value set, flag field for removal - if (config.remove_empty_image_field && !value && field.get('widget') === 'image') { - _pathsToRemove.add(newPath); - } - // If no serializer is registered for the field's widget, use the field as is if (!isNil(value)) { return acc.set(fieldName, value); @@ -71,20 +83,12 @@ function runSerializer(values, fields, method, config = {}, isRecursive = false, }, Map()); // preserve unknown fields value - serializedData = values.mergeDeep(serializedData); - - // Remove only on the top level, otherwise `mergeDeep` will reinsert them. - if (config.remove_empty_image_field && !isRecursive) { - serializedData = removeEntriesByPaths(serializedData, _pathsToRemove); - _pathsToRemove.clear(); - } - - return serializedData; + return values.mergeDeep(serializedData); } function removeEntriesByPaths(data, paths) { paths.forEach(path => { - data = removeEntryByPath(data, path.split('.')); + data = removeEntryByPath(data, path); }); return data; } @@ -106,7 +110,15 @@ function removeEntryByPath(data, keys) { } export function serializeValues(values, fields, config) { - return runSerializer(values, fields, 'serialize', config); + const pathsToRemove = []; + const serializedValues = runSerializer( + values, + fields, + 'serialize', + config?.remove_empty_fields, + pathsToRemove, + ); + return removeEntriesByPaths(serializedValues, pathsToRemove); } export function deserializeValues(values, fields) { diff --git a/packages/decap-cms-core/src/types/redux.ts b/packages/decap-cms-core/src/types/redux.ts index 1ff50f10f6a3..8aac4cdbfa3f 100644 --- a/packages/decap-cms-core/src/types/redux.ts +++ b/packages/decap-cms-core/src/types/redux.ts @@ -475,7 +475,7 @@ export interface CmsConfig { i18n?: CmsI18nConfig; issue_reports?: CmsIssueReports; local_backend?: boolean | CmsLocalBackend; - remove_empty_image_field?: boolean; + remove_empty_fields?: string[]; editor?: { preview?: boolean; notes?: boolean;