Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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');
});

Comment thread
oidacra marked this conversation as resolved.
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];

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,30 @@ const relationshipResolutionFn: FnResolutionValue<string> = (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<string | Record<string, unknown>> = (
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<string> = (contentlet, field) => {
const value = contentlet
? (contentlet[field.variable] ?? field.defaultValue)
Expand All @@ -229,12 +253,12 @@ const selectResolutionFn: FnResolutionValue<string> = (contentlet, field) => {
*/
export const resolutionValue: Record<
FIELD_TYPES,
FnResolutionValue<string | string[] | Date | number | null>
FnResolutionValue<string | string[] | Date | number | Record<string, unknown> | 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,
Expand Down
35 changes: 35 additions & 0 deletions core-web/libs/edit-content/src/lib/utils/functions.util.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down
22 changes: 18 additions & 4 deletions core-web/libs/edit-content/src/lib/utils/functions.util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -597,19 +597,20 @@ 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
* @param field - The field configuration
* @returns The processed field value
*/
export const processFieldValue = (
fieldValue: string | string[] | Date | number | null | undefined,
fieldValue: string | string[] | Date | number | Record<string, unknown> | 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
Expand All @@ -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;
};
Loading