From fb66f9aa9e3af3bcd45b267a67213bbf75ece488 Mon Sep 17 00:00:00 2001 From: Adrian Molina Date: Tue, 21 Apr 2026 11:40:11 -0400 Subject: [PATCH] fix(key-value): display null values as 'null' string after CSV import - Old Edit Content (JSP): remove null guard that silently dropped key-value pairs with null values; null is now rendered as the string "null" - New Edit Content (Angular): convert JSON null to the string "null" in parseToDotKeyValue so imported null values are visible in the UI - Stencil dot-key-value: add safety-net regex for ":null format in case the component receives raw HTML-encoded JSON with unquoted null Fixes #32823 Co-Authored-By: Claude Sonnet 4.6 --- .../dot-key-value/dot-key-value.e2e.ts | 22 +++++++++++++++++ .../dot-key-value/dot-key-value.tsx | 1 + .../key-value-field.component.ts | 2 +- ...t-edit-content-key-value.component.spec.ts | 4 ++-- .../dot-key-value-table-row.component.spec.ts | 24 +++++++++++++++++++ .../ext/contentlet/field/edit_field.jsp | 14 +++++------ 6 files changed, 57 insertions(+), 10 deletions(-) diff --git a/core-web/libs/dotcms-webcomponents/src/components/contenttypes-fields/dot-key-value/dot-key-value.e2e.ts b/core-web/libs/dotcms-webcomponents/src/components/contenttypes-fields/dot-key-value/dot-key-value.e2e.ts index c8951f2be65d..d560e5fa47f1 100644 --- a/core-web/libs/dotcms-webcomponents/src/components/contenttypes-fields/dot-key-value/dot-key-value.e2e.ts +++ b/core-web/libs/dotcms-webcomponents/src/components/contenttypes-fields/dot-key-value/dot-key-value.e2e.ts @@ -358,6 +358,28 @@ describe('dot-key-value', () => { ]); }); + it('should handle null values in HTML-encoded JSON', async () => { + element.setProperty('value', '{"my-value":null}'); + await page.waitForChanges(); + const list = await getList(); + expect(await list.getProperty('items')).toEqual([ + { key: 'my-value', value: 'null' } + ]); + }); + + it('should handle mixed null and non-null values in HTML-encoded JSON', async () => { + element.setProperty( + 'value', + '{"key1":null,"key2":"value2"}' + ); + await page.waitForChanges(); + const list = await getList(); + expect(await list.getProperty('items')).toEqual([ + { key: 'key1', value: 'null' }, + { key: 'key2', value: 'value2' } + ]); + }); + it('should handle invalid format', async () => { element.setProperty('value', 'hello/world*hola,mundo'); await page.waitForChanges(); diff --git a/core-web/libs/dotcms-webcomponents/src/components/contenttypes-fields/dot-key-value/dot-key-value.tsx b/core-web/libs/dotcms-webcomponents/src/components/contenttypes-fields/dot-key-value/dot-key-value.tsx index d33d2984a3c8..d772a932e08e 100644 --- a/core-web/libs/dotcms-webcomponents/src/components/contenttypes-fields/dot-key-value/dot-key-value.tsx +++ b/core-web/libs/dotcms-webcomponents/src/components/contenttypes-fields/dot-key-value/dot-key-value.tsx @@ -166,6 +166,7 @@ export class DotKeyValueComponent { formattedValue = this.value .replace(/</gi, '<') .replace(/[|]/gi, '|') + .replace(/":null/gi, '":"null"') .replace(/":"/gi, '|') .replace(/","/gi, ',') .replace(/{"/gi, '') diff --git a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-key-value/components/key-value-field/key-value-field.component.ts b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-key-value/components/key-value-field/key-value-field.component.ts index 045272a0946d..5c01df33e368 100644 --- a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-key-value/components/key-value-field/key-value-field.component.ts +++ b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-key-value/components/key-value-field/key-value-field.component.ts @@ -65,7 +65,7 @@ export class DotKeyValueFieldComponent extends BaseControlValueAccessor< return Object.keys(data).map((key: string) => ({ key, - value: data[key] ?? '' + value: data[key] === null ? 'null' : (data[key] ?? '') })); } diff --git a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-key-value/dot-edit-content-key-value.component.spec.ts b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-key-value/dot-edit-content-key-value.component.spec.ts index 7fe720abc80e..321862e78bcf 100644 --- a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-key-value/dot-edit-content-key-value.component.spec.ts +++ b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-key-value/dot-edit-content-key-value.component.spec.ts @@ -191,14 +191,14 @@ describe('DotEditContentKeyValueComponent', () => { expect(keyValueField.$initialValue()).toEqual([]); }); - it('should coalesce null values to empty string after import', () => { + it('should display null values as the string "null" after import', () => { const testData = { key1: null, key2: 'value2' }; const keyValueField = spectator.query(DotKeyValueFieldComponent); keyValueField.writeValue(testData); spectator.detectChanges(); expect(keyValueField.$initialValue()).toEqual([ - { key: 'key1', value: '' }, + { key: 'key1', value: 'null' }, { key: 'key2', value: 'value2' } ]); }); diff --git a/core-web/libs/ui/src/lib/components/dot-key-value-ng/dot-key-value-table-row/dot-key-value-table-row.component.spec.ts b/core-web/libs/ui/src/lib/components/dot-key-value-ng/dot-key-value-table-row/dot-key-value-table-row.component.spec.ts index 5cdc94be5461..0e1af42da7e5 100644 --- a/core-web/libs/ui/src/lib/components/dot-key-value-ng/dot-key-value-table-row/dot-key-value-table-row.component.spec.ts +++ b/core-web/libs/ui/src/lib/components/dot-key-value-ng/dot-key-value-table-row/dot-key-value-table-row.component.spec.ts @@ -240,6 +240,30 @@ describe('DotKeyValueTableRowComponent', () => { }); }); + describe('Null value displayed as "null" string (imported data)', () => { + beforeEach(() => { + spectator = createComponent({ + props: { + showHiddenField: false, + variable: { key: 'imported-key', hidden: false, value: 'null' }, + index: 0, + dragAndDrop: false + } as unknown + }); + spectator.detectChanges(); + }); + + it('should render the row and show "null" as value', () => { + const keyElement = spectator.query(byTestId('dot-key-value-key')); + const valueInput = spectator.query( + byTestId('dot-key-value-input') + ); + expect(keyElement.textContent).toContain('imported-key'); + expect(valueInput).toBeTruthy(); + expect(valueInput.value).toBe('null'); + }); + }); + describe('Drag and Drop', () => { beforeEach(() => { spectator = createComponent({ diff --git a/dotCMS/src/main/webapp/html/portlet/ext/contentlet/field/edit_field.jsp b/dotCMS/src/main/webapp/html/portlet/ext/contentlet/field/edit_field.jsp index f5bf364bb9c9..12eac42244ea 100644 --- a/dotCMS/src/main/webapp/html/portlet/ext/contentlet/field/edit_field.jsp +++ b/dotCMS/src/main/webapp/html/portlet/ext/contentlet/field/edit_field.jsp @@ -1484,13 +1484,13 @@ while (iterator.hasNext()) { final String key = iterator.next(); final Object object = keyValueMap.get(key); - if(null != object) { - keyValueDataRaw.append(key.replaceAll(":", ":").replaceAll(",", ",").replaceAll("<", "<")).append(":").append(object.toString().replaceAll(":", ":").replaceAll(",", ",").replaceAll("<", "<")); - dotKeyValueDataRaw.append(""" + key.replaceAll(":", ":").replaceAll(",", ",").replaceAll("<", "<") + """).append(":").append(""" + object.toString().replaceAll(":", ":").replaceAll(",", ",").replaceAll("<", "<") + """); - if (iterator.hasNext()) { - keyValueDataRaw.append(','); - dotKeyValueDataRaw.append(','); - } + final String encodedKey = key.replaceAll(":", ":").replaceAll(",", ",").replaceAll("<", "<"); + final String encodedValue = null != object ? object.toString().replaceAll(":", ":").replaceAll(",", ",").replaceAll("<", "<") : "null"; + keyValueDataRaw.append(encodedKey).append(":").append(encodedValue); + dotKeyValueDataRaw.append(""" + encodedKey + """).append(":").append(""" + encodedValue + """); + if (iterator.hasNext()) { + keyValueDataRaw.append(','); + dotKeyValueDataRaw.append(','); } } keyValueDataRaw.append("}");