diff --git a/core-web/libs/edit-content/src/lib/components/dot-edit-content-form/dot-edit-content-form-resolutions.spec.ts b/core-web/libs/edit-content/src/lib/components/dot-edit-content-form/dot-edit-content-form-resolutions.spec.ts index 3773e1b455b..e298e601178 100644 --- a/core-web/libs/edit-content/src/lib/components/dot-edit-content-form/dot-edit-content-form-resolutions.spec.ts +++ b/core-web/libs/edit-content/src/lib/components/dot-edit-content-form/dot-edit-content-form-resolutions.spec.ts @@ -406,7 +406,6 @@ describe('DotEditContentFormResolutions', () => { FIELD_TYPES.BINARY, FIELD_TYPES.FILE, FIELD_TYPES.IMAGE, - FIELD_TYPES.BLOCK_EDITOR, FIELD_TYPES.CHECKBOX, FIELD_TYPES.CONSTANT, FIELD_TYPES.CUSTOM_FIELD, @@ -425,6 +424,51 @@ describe('DotEditContentFormResolutions', () => { }); }); + describe('blockEditorResolutionFn', () => { + const blockField = { + ...mockField, + fieldType: FIELD_TYPES.BLOCK_EDITOR, + variable: 'blockContent' + } as DotCMSContentTypeField; + + it('should parse JSON string values from the API', () => { + const jsonObj = { type: 'doc', content: [{ type: 'paragraph' }] }; + const contentlet = { + ...mockContentlet, + blockContent: JSON.stringify(jsonObj) + }; + const result = resolutionValue[FIELD_TYPES.BLOCK_EDITOR](contentlet, blockField); + expect(result).toEqual(jsonObj); + }); + + it('should return object values as-is', () => { + const jsonObj = { type: 'doc', content: [{ type: 'paragraph' }] }; + const contentlet = { ...mockContentlet, blockContent: jsonObj }; + const result = resolutionValue[FIELD_TYPES.BLOCK_EDITOR]( + contentlet as unknown as DotCMSContentlet, + blockField + ); + expect(result).toEqual(jsonObj); + }); + + it('should return non-JSON strings as-is', () => { + const contentlet = { ...mockContentlet, blockContent: 'plain text' }; + const result = resolutionValue[FIELD_TYPES.BLOCK_EDITOR](contentlet, blockField); + expect(result).toBe('plain text'); + }); + + it('should return invalid JSON-looking strings as-is', () => { + const contentlet = { ...mockContentlet, blockContent: '{invalid' }; + const result = resolutionValue[FIELD_TYPES.BLOCK_EDITOR](contentlet, blockField); + expect(result).toBe('{invalid'); + }); + + it('should return defaultValue when contentlet is null', () => { + const result = resolutionValue[FIELD_TYPES.BLOCK_EDITOR](null, blockField); + expect(result).toBe(blockField.defaultValue); + }); + }); + it('should use dateResolutionFn for date field types', () => { const dateFieldTypes = [FIELD_TYPES.DATE, FIELD_TYPES.DATE_AND_TIME, FIELD_TYPES.TIME]; diff --git a/core-web/libs/edit-content/src/lib/components/dot-edit-content-form/dot-edit-content-form-resolutions.ts b/core-web/libs/edit-content/src/lib/components/dot-edit-content-form/dot-edit-content-form-resolutions.ts index 2646e4e4b7e..74e9ddad931 100644 --- a/core-web/libs/edit-content/src/lib/components/dot-edit-content-form/dot-edit-content-form-resolutions.ts +++ b/core-web/libs/edit-content/src/lib/components/dot-edit-content-form/dot-edit-content-form-resolutions.ts @@ -210,6 +210,30 @@ const relationshipResolutionFn: FnResolutionValue = (contentlet, field) return relationship.map((item) => item.identifier).join(','); }; +/** + * Resolution function for block editor fields. + * The API may return block editor content as a JSON string when copying/translating. + * This function parses the string to an object so the block editor component receives structured data. + */ +const blockEditorResolutionFn: FnResolutionValue> = ( + contentlet, + field +) => { + const value = contentlet + ? (contentlet[field.variable] ?? field.defaultValue) + : field.defaultValue; + + if (typeof value === 'string' && value.trim().startsWith('{')) { + try { + return JSON.parse(value); + } catch { + return value; + } + } + + return value; +}; + const selectResolutionFn: FnResolutionValue = (contentlet, field) => { const value = contentlet ? (contentlet[field.variable] ?? field.defaultValue) @@ -229,12 +253,12 @@ const selectResolutionFn: FnResolutionValue = (contentlet, field) => { */ export const resolutionValue: Record< FIELD_TYPES, - FnResolutionValue + FnResolutionValue | null> > = { [FIELD_TYPES.BINARY]: defaultResolutionFn, [FIELD_TYPES.FILE]: defaultResolutionFn, [FIELD_TYPES.IMAGE]: defaultResolutionFn, - [FIELD_TYPES.BLOCK_EDITOR]: defaultResolutionFn, + [FIELD_TYPES.BLOCK_EDITOR]: blockEditorResolutionFn, [FIELD_TYPES.CHECKBOX]: defaultResolutionFn, [FIELD_TYPES.CONSTANT]: defaultResolutionFn, [FIELD_TYPES.CUSTOM_FIELD]: defaultResolutionFn, diff --git a/core-web/libs/edit-content/src/lib/utils/functions.util.spec.ts b/core-web/libs/edit-content/src/lib/utils/functions.util.spec.ts index 5b4e6657d3b..d73cc6d5a57 100644 --- a/core-web/libs/edit-content/src/lib/utils/functions.util.spec.ts +++ b/core-web/libs/edit-content/src/lib/utils/functions.util.spec.ts @@ -1751,6 +1751,41 @@ describe('Utils Functions', () => { }); }); + describe('Block Editor fields', () => { + it('should stringify object values so the backend does not store them as Map.toString()', () => { + const field = { + fieldType: FIELD_TYPES.BLOCK_EDITOR, + variable: 'blockEditor' + } as unknown as DotCMSContentTypeField; + const objectValue = { + type: 'doc', + content: [{ type: 'paragraph' }] + }; + + expect(processFieldValue(objectValue, field)).toBe(JSON.stringify(objectValue)); + }); + + it('should pass through JSON string values unchanged', () => { + const field = { + fieldType: FIELD_TYPES.BLOCK_EDITOR, + variable: 'blockEditor' + } as unknown as DotCMSContentTypeField; + const stringValue = '{"type":"doc","content":[{"type":"paragraph"}]}'; + + expect(processFieldValue(stringValue, field)).toBe(stringValue); + }); + + it('should pass through null and undefined unchanged', () => { + const field = { + fieldType: FIELD_TYPES.BLOCK_EDITOR, + variable: 'blockEditor' + } as unknown as DotCMSContentTypeField; + + expect(processFieldValue(null, field)).toBeNull(); + expect(processFieldValue(undefined, field)).toBeUndefined(); + }); + }); + describe('edge cases', () => { it('should handle fields without specific processing', () => { const field = { diff --git a/core-web/libs/edit-content/src/lib/utils/functions.util.ts b/core-web/libs/edit-content/src/lib/utils/functions.util.ts index 5f2e8ed4123..f14bd21259c 100644 --- a/core-web/libs/edit-content/src/lib/utils/functions.util.ts +++ b/core-web/libs/edit-content/src/lib/utils/functions.util.ts @@ -505,7 +505,7 @@ export const createCustomFieldConfig = ( * @returns True if the field should be flattened */ export const isFlattenedField = ( - fieldValue: string | string[] | Date | number | null | undefined, + fieldValue: unknown, field: DotCMSContentTypeField ): fieldValue is string[] => { return ( @@ -533,7 +533,7 @@ export const isCalendarField = (field: DotCMSContentTypeField): boolean => { * @returns Numeric timestamp or null/undefined */ export const processCalendarFieldValue = ( - fieldValue: string | string[] | Date | number | null | undefined, + fieldValue: unknown, fieldName: string ): number | null | undefined => { // Handle null/undefined values @@ -597,6 +597,7 @@ export const processCalendarFieldValue = ( * Applies appropriate transformations for different field types: * - Flattened fields: Joins arrays with commas * - Calendar fields: Converts to numeric timestamps + * - Block Editor: Stringifies object values (see details below) * - Other fields: Returns as-is * * @param fieldValue - The raw field value @@ -604,12 +605,12 @@ export const processCalendarFieldValue = ( * @returns The processed field value */ export const processFieldValue = ( - fieldValue: string | string[] | Date | number | null | undefined, + fieldValue: string | string[] | Date | number | Record | null | undefined, field: DotCMSContentTypeField ): string | number | null | undefined => { // Handle flattened fields (multi-select, etc.) if (isFlattenedField(fieldValue, field)) { - return (fieldValue as string[]).join(','); + return fieldValue.join(','); } // Handle category fields: join inode array into comma-separated string for the API @@ -622,6 +623,19 @@ export const processFieldValue = ( return processCalendarFieldValue(fieldValue, field.variable); } + // Handle Block Editor: the FormControl may hold an object when the form is + // initialized from a translated contentlet (blockEditorResolutionFn parses + // the JSON string to an object so the editor can render it). The backend + // expects a JSON string — sending an object causes it to be stored as + // Map.toString(), corrupting the field on save. + if ( + field.fieldType === FIELD_TYPES.BLOCK_EDITOR && + fieldValue && + typeof fieldValue === 'object' + ) { + return JSON.stringify(fieldValue); + } + // For all other fields, return as-is return fieldValue as string | number | null | undefined; };