RICH_TEXT_V2 backend (#9848)

- Add RICH_TEXT_V2 composite type to backend.
- Add `bodyV2` field to tasks and notes.
- Minimum required frontend changes to avoid errors when creating a note

[Testing
instructions](https://github.com/twentyhq/twenty/pull/9690#issuecomment-2602378218)

---------

Co-authored-by: ad-elias <elias@autodiligence.com>
Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com>
This commit is contained in:
eliasylonen
2025-01-28 14:05:06 +01:00
committed by GitHub
parent 6f72f1af33
commit b63ae14318
34 changed files with 517 additions and 102 deletions

View File

@ -288,6 +288,7 @@ export const NOTE_STANDARD_FIELD_IDS = {
position: '20202020-368d-4dc2-943f-ed8a49c7fdfb',
title: '20202020-faeb-4c76-8ba6-ccbb0b4a965f',
body: '20202020-e63d-4e70-95be-a78cd9abe7ef',
bodyV2: '20202020-a7bb-4d94-be51-8f25181502c8',
createdBy: '20202020-0d79-4e21-ab77-5a394eff97be',
noteTargets: '20202020-1f25-43fe-8b00-af212fdde823',
attachments: '20202020-4986-4c92-bf19-39934b149b16',
@ -355,6 +356,7 @@ export const TASK_STANDARD_FIELD_IDS = {
position: '20202020-7d47-4690-8a98-98b9a0c05dd8',
title: '20202020-b386-4cb7-aa5a-08d4a4d92680',
body: '20202020-ce13-43f4-8821-69388fe1fd26',
bodyV2: '20202020-4aa0-4ae8-898d-7df0afd47ab1',
dueAt: '20202020-fd99-40da-951b-4cb9a352fce3',
status: '20202020-70bc-48f9-89c5-6aa730b151e0',
createdBy: '20202020-1a04-48ab-a567-576965ae5387',

View File

@ -13,58 +13,6 @@ const nameFullNameField = {
const jobTitleTextField = { name: 'jobTitle', type: FieldMetadataType.TEXT };
const emailsEmailsField = { name: 'emails', type: FieldMetadataType.EMAILS };
jest.mock(
'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util',
() => ({
computeColumnName: jest.fn((name) => {
if (name === 'name') {
return 'name';
}
if (name === 'jobTitle') {
return 'jobTitle';
}
if (name === 'emailsPrimaryEmail') {
return 'emailsPrimaryEmail';
}
if (name === 'emailsAdditionalEmails') {
return 'emailsAdditionalEmails';
}
if (name === 'nameFirstName') {
return 'nameFirstName';
}
if (name === 'nameLastName') {
return 'nameLastName';
}
}),
computeCompositeColumnName: jest.fn((field, property) => {
if (
field.name === emailsEmailsField.name &&
property.name === 'primaryEmail'
) {
return 'emailsPrimaryEmail';
}
if (
field.name === emailsEmailsField.name &&
property.name === 'additionalEmails'
) {
return 'emailsAdditionalEmails';
}
if (
field.name === nameFullNameField.name &&
property.name === 'firstName'
) {
return 'nameFirstName';
}
if (
field.name === nameFullNameField.name &&
property.name === 'lastName'
) {
return 'nameLastName';
}
}),
}),
);
describe('getTsVectorColumnExpressionFromFields', () => {
it('should generate correct expression for simple text field', () => {
const fields = [nameTextField] as FieldTypeAndNameMetadata[];
@ -95,4 +43,24 @@ describe('getTsVectorColumnExpressionFromFields', () => {
expect(result.trim()).toBe(expected);
});
it('should handle rich text fields', () => {
const fields = [
{ name: 'body', type: FieldMetadataType.RICH_TEXT },
] as FieldTypeAndNameMetadata[];
const result = getTsVectorColumnExpressionFromFields(fields);
expect(result).toBe("to_tsvector('simple', COALESCE(\"body\", ''))");
});
it('should handle rich text v2 fields', () => {
const fields = [
{ name: 'bodyV2', type: FieldMetadataType.RICH_TEXT_V2 },
] as FieldTypeAndNameMetadata[];
const result = getTsVectorColumnExpressionFromFields(fields);
expect(result).toBe(
"to_tsvector('simple', COALESCE(\"bodyV2Markdown\", ''))",
);
});
});

View File

@ -14,6 +14,7 @@ import {
isSearchableFieldType,
SearchableFieldType,
} from 'src/engine/workspace-manager/workspace-sync-metadata/utils/is-searchable-field.util';
import { isSearchableSubfield } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/is-searchable-subfield.util';
export type FieldTypeAndNameMetadata = {
name: string;
@ -55,7 +56,9 @@ const getColumnExpressionsFromField = (
}
return compositeType.properties
.filter((property) => property.type === FieldMetadataType.TEXT)
.filter((property) =>
isSearchableSubfield(compositeType.type, property.type, property.name),
)
.map((property) => {
const columnName = computeCompositeColumnName(
fieldMetadataTypeAndName,

View File

@ -7,6 +7,7 @@ const SEARCHABLE_FIELD_TYPES = [
FieldMetadataType.ADDRESS,
FieldMetadataType.LINKS,
FieldMetadataType.RICH_TEXT,
FieldMetadataType.RICH_TEXT_V2,
] as const;
export type SearchableFieldType = (typeof SEARCHABLE_FIELD_TYPES)[number];

View File

@ -0,0 +1,18 @@
import { FieldMetadataType } from 'twenty-shared';
export const isSearchableSubfield = (
compositeFieldMetadataType: FieldMetadataType,
subFieldMetadataType: FieldMetadataType,
subFieldName: string,
) => {
if (subFieldMetadataType !== FieldMetadataType.TEXT) {
return false;
}
switch (compositeFieldMetadataType) {
case FieldMetadataType.RICH_TEXT_V2:
return ['markdown'].includes(subFieldName);
default:
return true;
}
};