From b63ae14318cc4d65ef7bec28ee896e2ec7c20588 Mon Sep 17 00:00:00 2001 From: eliasylonen Date: Tue, 28 Jan 2025 14:05:06 +0100 Subject: [PATCH] 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 Co-authored-by: Lucas Bordeau --- package.json | 1 + .../src/generated-metadata/graphql.ts | 1 + .../twenty-front/src/generated/graphql.tsx | 2 + .../record-field/types/FieldMetadata.ts | 1 + .../types/SettingsExcludedFieldType.ts | 2 +- .../constants/DefaultIconsByFieldType.ts | 1 + .../typeorm-seeds/core/feature-flags.ts | 5 + .../activity-query-result-getter.handler.ts | 36 +++- .../query-result-getters.factory.ts | 22 ++- .../factories/query-runner-args.factory.ts | 128 ++++++++++---- .../input/rich-text.input-type.ts | 16 ++ .../services/type-mapper.service.ts | 2 + ...p-field-metadata-to-graphql-query.utils.ts | 8 + .../enums/feature-flag-key.enum.ts | 1 + .../open-api/utils/components.utils.ts | 13 ++ .../field-metadata/composite-types/index.ts | 2 + .../rich-text-v2.composite-type.ts | 29 ++++ .../dtos/default-value.input.ts | 11 ++ .../__tests__/compute-column-name.spec.ts | 23 +++ .../utils/generate-default-value.ts | 5 + .../is-composite-field-metadata-type.util.ts | 4 +- .../validate-default-value-for-type.util.ts | 2 + .../composite-column-action.factory.ts | 3 +- .../workspace-migration.factory.ts | 4 + ...-subfields-for-aggregate-operation.util.ts | 2 + .../constants/standard-field-ids.ts | 2 + ...ts-vectors-column-expression.utils.spec.ts | 72 +++----- .../get-ts-vector-column-expression.util.ts | 5 +- .../utils/is-searchable-field.util.ts | 1 + .../utils/is-searchable-subfield.util.ts | 18 ++ .../standard-objects/note.workspace-entity.ts | 18 +- .../standard-objects/task.workspace-entity.ts | 18 +- .../src/types/FieldMetadataType.ts | 1 + yarn.lock | 160 +++++++++++++++++- 34 files changed, 517 insertions(+), 102 deletions(-) create mode 100644 packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-types/input/rich-text.input-type.ts create mode 100644 packages/twenty-server/src/engine/metadata-modules/field-metadata/composite-types/rich-text-v2.composite-type.ts create mode 100644 packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/__tests__/compute-column-name.spec.ts create mode 100644 packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/utils/is-searchable-subfield.util.ts diff --git a/package.json b/package.json index 3f880ff24..58084915b 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "@aws-sdk/credential-providers": "^3.363.0", "@blocknote/mantine": "^0.22.0", "@blocknote/react": "^0.22.0", + "@blocknote/server-util": "0.17.1", "@codesandbox/sandpack-react": "^2.13.5", "@dagrejs/dagre": "^1.1.2", "@emotion/react": "^11.11.1", diff --git a/packages/twenty-front/src/generated-metadata/graphql.ts b/packages/twenty-front/src/generated-metadata/graphql.ts index 8f3fb7728..5224019f1 100644 --- a/packages/twenty-front/src/generated-metadata/graphql.ts +++ b/packages/twenty-front/src/generated-metadata/graphql.ts @@ -541,6 +541,7 @@ export enum FieldMetadataType { RAW_JSON = 'RAW_JSON', RELATION = 'RELATION', RICH_TEXT = 'RICH_TEXT', + RICH_TEXT_V2 = 'RICH_TEXT_V2', SELECT = 'SELECT', TEXT = 'TEXT', TS_VECTOR = 'TS_VECTOR', diff --git a/packages/twenty-front/src/generated/graphql.tsx b/packages/twenty-front/src/generated/graphql.tsx index 9f4e9fd67..43f88c2c8 100644 --- a/packages/twenty-front/src/generated/graphql.tsx +++ b/packages/twenty-front/src/generated/graphql.tsx @@ -396,6 +396,7 @@ export enum FeatureFlagKey { IsMicrosoftSyncEnabled = 'IsMicrosoftSyncEnabled', IsNewRelationEnabled = 'IsNewRelationEnabled', IsPostgreSQLIntegrationEnabled = 'IsPostgreSQLIntegrationEnabled', + IsRichTextV2Enabled = 'IsRichTextV2Enabled', IsStripeIntegrationEnabled = 'IsStripeIntegrationEnabled', IsUniqueIndexesEnabled = 'IsUniqueIndexesEnabled', IsWorkflowEnabled = 'IsWorkflowEnabled' @@ -473,6 +474,7 @@ export enum FieldMetadataType { RAW_JSON = 'RAW_JSON', RELATION = 'RELATION', RICH_TEXT = 'RICH_TEXT', + RICH_TEXT_V2 = 'RICH_TEXT_V2', SELECT = 'SELECT', TEXT = 'TEXT', TS_VECTOR = 'TS_VECTOR', diff --git a/packages/twenty-front/src/modules/object-record/record-field/types/FieldMetadata.ts b/packages/twenty-front/src/modules/object-record/record-field/types/FieldMetadata.ts index c04cca016..ef94641c2 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/types/FieldMetadata.ts +++ b/packages/twenty-front/src/modules/object-record/record-field/types/FieldMetadata.ts @@ -203,6 +203,7 @@ export type FieldMetadata = | FieldPhoneMetadata | FieldRatingMetadata | FieldRelationMetadata + | FieldRichTextMetadata | FieldSelectMetadata | FieldMultiSelectMetadata | FieldTextMetadata diff --git a/packages/twenty-front/src/modules/settings/data-model/types/SettingsExcludedFieldType.ts b/packages/twenty-front/src/modules/settings/data-model/types/SettingsExcludedFieldType.ts index 3c7041e9f..e4c033041 100644 --- a/packages/twenty-front/src/modules/settings/data-model/types/SettingsExcludedFieldType.ts +++ b/packages/twenty-front/src/modules/settings/data-model/types/SettingsExcludedFieldType.ts @@ -3,5 +3,5 @@ import { PickLiteral } from '~/types/PickLiteral'; export type SettingsExcludedFieldType = PickLiteral< FieldType, - 'POSITION' | 'TS_VECTOR' + 'POSITION' | 'TS_VECTOR' | 'RICH_TEXT_V2' >; diff --git a/packages/twenty-front/src/pages/settings/data-model/constants/DefaultIconsByFieldType.ts b/packages/twenty-front/src/pages/settings/data-model/constants/DefaultIconsByFieldType.ts index 1e060907f..538210b17 100644 --- a/packages/twenty-front/src/pages/settings/data-model/constants/DefaultIconsByFieldType.ts +++ b/packages/twenty-front/src/pages/settings/data-model/constants/DefaultIconsByFieldType.ts @@ -23,5 +23,6 @@ export const DEFAULT_ICONS_BY_FIELD_TYPE: Record = { [FieldMetadataType.NUMERIC]: 'IconUsers', [FieldMetadataType.POSITION]: 'IconUsers', [FieldMetadataType.RICH_TEXT]: 'IconUsers', + [FieldMetadataType.RICH_TEXT_V2]: 'IconUsers', [FieldMetadataType.TS_VECTOR]: 'IconUsers', }; diff --git a/packages/twenty-server/src/database/typeorm-seeds/core/feature-flags.ts b/packages/twenty-server/src/database/typeorm-seeds/core/feature-flags.ts index de20550d1..9f8680ef4 100644 --- a/packages/twenty-server/src/database/typeorm-seeds/core/feature-flags.ts +++ b/packages/twenty-server/src/database/typeorm-seeds/core/feature-flags.ts @@ -75,6 +75,11 @@ export const seedFeatureFlags = async ( workspaceId: workspaceId, value: true, }, + { + key: FeatureFlagKey.IsRichTextV2Enabled, + workspaceId: workspaceId, + value: false, + }, { key: FeatureFlagKey.IsNewRelationEnabled, workspaceId: workspaceId, diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/handlers/activity-query-result-getter.handler.ts b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/handlers/activity-query-result-getter.handler.ts index f8dceb092..59b8d0b04 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/handlers/activity-query-result-getter.handler.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/handlers/activity-query-result-getter.handler.ts @@ -1,5 +1,7 @@ import { QueryResultGetterHandlerInterface } from 'src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/interfaces/query-result-getter-handler.interface'; +import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; +import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service'; import { FileService } from 'src/engine/core-modules/file/services/file.service'; import { NoteWorkspaceEntity } from 'src/modules/note/standard-objects/note.workspace-entity'; import { TaskWorkspaceEntity } from 'src/modules/task/standard-objects/task.workspace-entity'; @@ -11,20 +13,32 @@ type RichTextBody = RichTextBlock[]; export class ActivityQueryResultGetterHandler implements QueryResultGetterHandlerInterface { - constructor(private readonly fileService: FileService) {} + constructor( + private readonly fileService: FileService, + private readonly featureFlagService: FeatureFlagService, + ) {} async handle( activity: TaskWorkspaceEntity | NoteWorkspaceEntity, workspaceId: string, ): Promise { - if (!activity.id || !activity.body) { + const isRichTextV2Enabled = await this.featureFlagService.isFeatureEnabled( + FeatureFlagKey.IsRichTextV2Enabled, + workspaceId, + ); + + const blocknoteJson = isRichTextV2Enabled + ? activity.bodyV2?.blocknote + : activity.body; + + if (!activity.id || !blocknoteJson) { return activity; } - const body: RichTextBody = JSON.parse(activity.body); + const blocknote: RichTextBody = JSON.parse(blocknoteJson); - const bodyWithSignedPayload = await Promise.all( - body.map(async (block: RichTextBlock) => { + const blocknoteWithSignedPayload = await Promise.all( + blocknote.map(async (block: RichTextBlock) => { if (block.type !== 'image' || !block.props.url) { return block; } @@ -49,9 +63,19 @@ export class ActivityQueryResultGetterHandler }), ); + if (isRichTextV2Enabled) { + return { + ...activity, + bodyV2: { + blocknote: JSON.stringify(blocknoteWithSignedPayload), + markdown: activity.bodyV2?.markdown ?? null, + }, + }; + } + return { ...activity, - body: JSON.stringify(bodyWithSignedPayload), + body: JSON.stringify(blocknoteWithSignedPayload), }; } } diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/query-result-getters.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/query-result-getters.factory.ts index 3602fb235..3422fb480 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/query-result-getters.factory.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/query-result-getters.factory.ts @@ -16,6 +16,7 @@ import { AttachmentQueryResultGetterHandler } from 'src/engine/api/graphql/works import { PersonQueryResultGetterHandler } from 'src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/handlers/person-query-result-getter.handler'; import { WorkspaceMemberQueryResultGetterHandler } from 'src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/handlers/workspace-member-query-result-getter.handler'; import { CompositeInputTypeDefinitionFactory } from 'src/engine/api/graphql/workspace-schema-builder/factories/composite-input-type-definition.factory'; +import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service'; import { FileService } from 'src/engine/core-modules/file/services/file.service'; import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; import { isRelationFieldMetadataType } from 'src/engine/utils/is-relation-field-metadata-type.util'; @@ -31,7 +32,10 @@ export class QueryResultGettersFactory { ); private handlers: Map; - constructor(private readonly fileService: FileService) { + constructor( + private readonly fileService: FileService, + private readonly featureFlagService: FeatureFlagService, + ) { this.initializeHandlers(); } @@ -43,8 +47,20 @@ export class QueryResultGettersFactory { 'workspaceMember', new WorkspaceMemberQueryResultGetterHandler(this.fileService), ], - ['note', new ActivityQueryResultGetterHandler(this.fileService)], - ['task', new ActivityQueryResultGetterHandler(this.fileService)], + [ + 'note', + new ActivityQueryResultGetterHandler( + this.fileService, + this.featureFlagService, + ), + ], + [ + 'task', + new ActivityQueryResultGetterHandler( + this.fileService, + this.featureFlagService, + ), + ], ]); } diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/factories/query-runner-args.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/factories/query-runner-args.factory.ts index 25cab9225..af1d51875 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/factories/query-runner-args.factory.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/factories/query-runner-args.factory.ts @@ -1,5 +1,6 @@ import { Injectable } from '@nestjs/common'; +import { ServerBlockNoteEditor } from '@blocknote/server-util'; import { FieldMetadataType } from 'twenty-shared'; import { @@ -15,9 +16,15 @@ import { FindOneResolverArgs, ResolverArgs, ResolverArgsType, + UpdateManyResolverArgs, + UpdateOneResolverArgs, } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface'; import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface'; +import { + RichTextV2Metadata, + richTextV2ValueSchema, +} from 'src/engine/metadata-modules/field-metadata/composite-types/rich-text-v2.composite-type'; import { FieldMetadataMap } from 'src/engine/metadata-modules/types/field-metadata-map'; import { RecordPositionFactory } from './record-position.factory'; @@ -77,6 +84,41 @@ export class QueryRunnerArgsFactory { ) ?? [], ), } satisfies CreateManyResolverArgs; + case ResolverArgsType.UpdateOne: + return { + ...args, + id: (args as UpdateOneResolverArgs).id, + data: await this.overrideDataByFieldMetadata( + (args as UpdateOneResolverArgs).data, + options, + fieldMetadataMapByNameByName, + { + argIndex: 0, + shouldBackfillPosition, + }, + ), + } satisfies UpdateOneResolverArgs; + case ResolverArgsType.UpdateMany: + return { + ...args, + filter: await this.overrideFilterByFieldMetadata( + (args as UpdateManyResolverArgs).filter, + fieldMetadataMapByNameByName, + ), + data: await Promise.all( + (args as UpdateManyResolverArgs).data?.map((arg, index) => + this.overrideDataByFieldMetadata( + arg, + options, + fieldMetadataMapByNameByName, + { + argIndex: index, + shouldBackfillPosition, + }, + ), + ) ?? [], + ), + } satisfies UpdateManyResolverArgs; case ResolverArgsType.FindOne: return { ...args, @@ -130,47 +172,73 @@ export class QueryRunnerArgsFactory { options: WorkspaceQueryRunnerOptions, fieldMetadataMapByNameByName: Record, argPositionBackfillInput: ArgPositionBackfillInput, - ) { + ): Promise> { if (!data) { - return; + return Promise.resolve({}); } let isFieldPositionPresent = false; - const createArgPromiseByArgKey = Object.entries(data).map( - async ([key, value]) => { - const fieldMetadata = fieldMetadataMapByNameByName[key]; + const createArgByArgKeyPromises: Promise<[string, any]>[] = Object.entries( + data, + ).map(async ([key, value]): Promise<[string, any]> => { + const fieldMetadata = fieldMetadataMapByNameByName[key]; - if (!fieldMetadata) { - return [key, await Promise.resolve(value)]; + if (!fieldMetadata) { + return [key, value]; + } + + switch (fieldMetadata.type) { + case FieldMetadataType.POSITION: { + isFieldPositionPresent = true; + + const newValue = await this.recordPositionFactory.create( + value, + { + isCustom: options.objectMetadataItemWithFieldMaps.isCustom, + nameSingular: + options.objectMetadataItemWithFieldMaps.nameSingular, + }, + options.authContext.workspace.id, + argPositionBackfillInput.argIndex, + ); + + return [key, newValue]; } + case FieldMetadataType.NUMBER: + return [key, Number(value)] as const; + case FieldMetadataType.RICH_TEXT_V2: { + const richTextV2Value = richTextV2ValueSchema.parse(value); - switch (fieldMetadata.type) { - case FieldMetadataType.POSITION: - isFieldPositionPresent = true; + const serverBlockNoteEditor = ServerBlockNoteEditor.create(); - return [ - key, - await this.recordPositionFactory.create( - value, - { - isCustom: options.objectMetadataItemWithFieldMaps.isCustom, - nameSingular: - options.objectMetadataItemWithFieldMaps.nameSingular, - }, - options.authContext.workspace.id, - argPositionBackfillInput.argIndex, - ), - ]; - case FieldMetadataType.NUMBER: - return [key, Number(value)]; - default: - return [key, await Promise.resolve(value)]; + const convertedMarkdown = richTextV2Value.blocknote + ? await serverBlockNoteEditor.blocksToMarkdownLossy( + JSON.parse(richTextV2Value.blocknote), + ) + : null; + + const convertedBlocknote = richTextV2Value.markdown + ? JSON.stringify( + await serverBlockNoteEditor.tryParseMarkdownToBlocks( + richTextV2Value.markdown, + ), + ) + : null; + + const valueInBothFormats: RichTextV2Metadata = { + markdown: richTextV2Value.markdown || convertedMarkdown, + blocknote: richTextV2Value.blocknote || convertedBlocknote, + }; + + return [key, valueInBothFormats]; } - }, - ); + default: + return [key, value]; + } + }); - const newArgEntries = await Promise.all(createArgPromiseByArgKey); + const newArgEntries = await Promise.all(createArgByArgKeyPromises); if ( !isFieldPositionPresent && diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-types/input/rich-text.input-type.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-types/input/rich-text.input-type.ts new file mode 100644 index 000000000..e99353b16 --- /dev/null +++ b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-types/input/rich-text.input-type.ts @@ -0,0 +1,16 @@ +import { GraphQLInputObjectType, GraphQLString } from 'graphql'; + +const richTextV2LeafFilter = new GraphQLInputObjectType({ + name: 'RichTextV2LeafFilter', + fields: { + ilike: { type: GraphQLString }, + }, +}); + +export const RichTextV2FilterType = new GraphQLInputObjectType({ + name: 'RichTextV2Filter', + fields: { + blocknote: { type: richTextV2LeafFilter }, + markdown: { type: richTextV2LeafFilter }, + }, +}); diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/services/type-mapper.service.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/services/type-mapper.service.ts index 4970dc3f5..c53658ffc 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/services/type-mapper.service.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/services/type-mapper.service.ts @@ -29,6 +29,7 @@ import { } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/input'; import { IDFilterType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/input/id-filter.input-type'; import { MultiSelectFilterType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/input/multi-select-filter.input-type'; +import { RichTextV2FilterType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/input/rich-text.input-type'; import { SelectFilterType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/input/select-filter.input-type'; import { BigFloatScalarType, @@ -116,6 +117,7 @@ export class TypeMapperService { [FieldMetadataType.POSITION, FloatFilterType], [FieldMetadataType.RAW_JSON, RawJsonFilterType], [FieldMetadataType.RICH_TEXT, StringFilterType], + [FieldMetadataType.RICH_TEXT_V2, RichTextV2FilterType], [FieldMetadataType.ARRAY, ArrayFilterType], [FieldMetadataType.MULTI_SELECT, MultiSelectFilterType], [FieldMetadataType.SELECT, SelectFilterType], diff --git a/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/map-field-metadata-to-graphql-query.utils.ts b/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/map-field-metadata-to-graphql-query.utils.ts index 96bacd162..885d215c1 100644 --- a/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/map-field-metadata-to-graphql-query.utils.ts +++ b/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/map-field-metadata-to-graphql-query.utils.ts @@ -155,5 +155,13 @@ export const mapFieldMetadataToGraphqlQuery = ( additionalPhones } `; + } else if (fieldType === FieldMetadataType.RICH_TEXT_V2) { + return ` + ${field.name} + { + blocknote + markdown + } + `; } }; diff --git a/packages/twenty-server/src/engine/core-modules/feature-flag/enums/feature-flag-key.enum.ts b/packages/twenty-server/src/engine/core-modules/feature-flag/enums/feature-flag-key.enum.ts index b23ea9c81..d9bc56844 100644 --- a/packages/twenty-server/src/engine/core-modules/feature-flag/enums/feature-flag-key.enum.ts +++ b/packages/twenty-server/src/engine/core-modules/feature-flag/enums/feature-flag-key.enum.ts @@ -13,5 +13,6 @@ export enum FeatureFlagKey { IsJsonFilterEnabled = 'IS_JSON_FILTER_ENABLED', IsLocalizationEnabled = 'IS_LOCALIZATION_ENABLED', IsBillingPlansEnabled = 'IS_BILLING_PLANS_ENABLED', + IsRichTextV2Enabled = 'IS_RICH_TEXT_V2_ENABLED', IsNewRelationEnabled = 'IS_NEW_RELATION_ENABLED', } diff --git a/packages/twenty-server/src/engine/core-modules/open-api/utils/components.utils.ts b/packages/twenty-server/src/engine/core-modules/open-api/utils/components.utils.ts index e01c55257..f32903d72 100644 --- a/packages/twenty-server/src/engine/core-modules/open-api/utils/components.utils.ts +++ b/packages/twenty-server/src/engine/core-modules/open-api/utils/components.utils.ts @@ -266,6 +266,19 @@ const getSchemaComponentsProperties = ({ type: 'object', }; break; + case FieldMetadataType.RICH_TEXT_V2: + itemProperty = { + type: 'object', + properties: { + blocknote: { + type: 'string', + }, + markdown: { + type: 'string', + }, + }, + }; + break; default: itemProperty = getFieldProperties(field.type); break; diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/composite-types/index.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/composite-types/index.ts index f7f943b82..aed105c3c 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/composite-types/index.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/composite-types/index.ts @@ -9,6 +9,7 @@ import { emailsCompositeType } from 'src/engine/metadata-modules/field-metadata/ import { fullNameCompositeType } from 'src/engine/metadata-modules/field-metadata/composite-types/full-name.composite-type'; import { linksCompositeType } from 'src/engine/metadata-modules/field-metadata/composite-types/links.composite-type'; import { phonesCompositeType } from 'src/engine/metadata-modules/field-metadata/composite-types/phones.composite-type'; +import { richTextV2CompositeType } from 'src/engine/metadata-modules/field-metadata/composite-types/rich-text-v2.composite-type'; export const compositeTypeDefinitions = new Map< FieldMetadataType, @@ -21,4 +22,5 @@ export const compositeTypeDefinitions = new Map< [FieldMetadataType.ACTOR, actorCompositeType], [FieldMetadataType.EMAILS, emailsCompositeType], [FieldMetadataType.PHONES, phonesCompositeType], + [FieldMetadataType.RICH_TEXT_V2, richTextV2CompositeType], ]); diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/composite-types/rich-text-v2.composite-type.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/composite-types/rich-text-v2.composite-type.ts new file mode 100644 index 000000000..0d7a98ced --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/composite-types/rich-text-v2.composite-type.ts @@ -0,0 +1,29 @@ +import { FieldMetadataType } from 'twenty-shared'; +import { z } from 'zod'; + +import { CompositeType } from 'src/engine/metadata-modules/field-metadata/interfaces/composite-type.interface'; + +export const richTextV2CompositeType: CompositeType = { + type: FieldMetadataType.RICH_TEXT_V2, + properties: [ + { + name: 'blocknote', + type: FieldMetadataType.TEXT, + hidden: false, + isRequired: false, + }, + { + name: 'markdown', + type: FieldMetadataType.TEXT, + hidden: false, + isRequired: false, + }, + ], +}; + +export const richTextV2ValueSchema = z.object({ + blocknote: z.string().nullable(), + markdown: z.string().nullable(), +}); + +export type RichTextV2Metadata = z.infer; diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/dtos/default-value.input.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/dtos/default-value.input.ts index 68b88bc28..4479d0260 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/dtos/default-value.input.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/dtos/default-value.input.ts @@ -35,11 +35,22 @@ export class FieldMetadataDefaultValueRawJson { value: object | null; } +export class FieldMetadataDefaultValueRichTextV2 { + @ValidateIf((object, value) => value !== null) + @IsQuotedString() + blocknote: string | null; + + @ValidateIf((object, value) => value !== null) + @IsQuotedString() + markdown: string | null; +} + export class FieldMetadataDefaultValueRichText { @ValidateIf((_object, value) => value !== null) @IsString() value: string | null; } + export class FieldMetadataDefaultValueNumber { @ValidateIf((object, value) => value !== null) @IsNumber() diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/__tests__/compute-column-name.spec.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/__tests__/compute-column-name.spec.ts new file mode 100644 index 000000000..aaf9f62be --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/__tests__/compute-column-name.spec.ts @@ -0,0 +1,23 @@ +import { FieldMetadataType } from 'twenty-shared'; + +import { computeCompositeColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util'; + +describe('computeCompositeColumnName', () => { + it('should compute composite column name for rich text v2 field', () => { + const fieldMetadata = { + name: 'bodyV2', + type: FieldMetadataType.RICH_TEXT_V2, + }; + + const property = { + name: 'markdown', + type: FieldMetadataType.TEXT, + hidden: false, + isRequired: false, + }; + + expect(computeCompositeColumnName(fieldMetadata, property)).toEqual( + 'bodyV2Markdown', + ); + }); +}); diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/generate-default-value.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/generate-default-value.ts index bbb307ebf..d2e3d2bd7 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/generate-default-value.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/generate-default-value.ts @@ -47,6 +47,11 @@ export function generateDefaultValue( primaryPhoneCallingCode: "''", additionalPhones: null, }; + case FieldMetadataType.RICH_TEXT_V2: + return { + blocknote: "''", + markdown: "''", + }; default: return null; } diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util.ts index 0fb1c6968..172751bb0 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util.ts @@ -9,7 +9,8 @@ export const isCompositeFieldMetadataType = ( | FieldMetadataType.LINKS | FieldMetadataType.ACTOR | FieldMetadataType.EMAILS - | FieldMetadataType.PHONES => { + | FieldMetadataType.PHONES + | FieldMetadataType.RICH_TEXT_V2 => { return [ FieldMetadataType.CURRENCY, FieldMetadataType.FULL_NAME, @@ -18,5 +19,6 @@ export const isCompositeFieldMetadataType = ( FieldMetadataType.ACTOR, FieldMetadataType.EMAILS, FieldMetadataType.PHONES, + FieldMetadataType.RICH_TEXT_V2, ].includes(type); }; diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/validate-default-value-for-type.util.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/validate-default-value-for-type.util.ts index efa979312..c495836e8 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/validate-default-value-for-type.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/validate-default-value-for-type.util.ts @@ -21,6 +21,7 @@ import { FieldMetadataDefaultValueNumber, FieldMetadataDefaultValuePhones, FieldMetadataDefaultValueRawJson, + FieldMetadataDefaultValueRichTextV2, FieldMetadataDefaultValueString, FieldMetadataDefaultValueStringArray, FieldMetadataDefaultValueUuidFunction, @@ -47,6 +48,7 @@ export const defaultValueValidatorsMap = { [FieldMetadataType.SELECT]: [FieldMetadataDefaultValueString], [FieldMetadataType.MULTI_SELECT]: [FieldMetadataDefaultValueStringArray], [FieldMetadataType.ADDRESS]: [FieldMetadataDefaultValueAddress], + [FieldMetadataType.RICH_TEXT_V2]: [FieldMetadataDefaultValueRichTextV2], [FieldMetadataType.RICH_TEXT]: [FieldMetadataDefaultValueString], [FieldMetadataType.RAW_JSON]: [FieldMetadataDefaultValueRawJson], [FieldMetadataType.LINKS]: [FieldMetadataDefaultValueLinks], diff --git a/packages/twenty-server/src/engine/metadata-modules/workspace-migration/factories/composite-column-action.factory.ts b/packages/twenty-server/src/engine/metadata-modules/workspace-migration/factories/composite-column-action.factory.ts index 941403074..830e62281 100644 --- a/packages/twenty-server/src/engine/metadata-modules/workspace-migration/factories/composite-column-action.factory.ts +++ b/packages/twenty-server/src/engine/metadata-modules/workspace-migration/factories/composite-column-action.factory.ts @@ -26,7 +26,8 @@ export type CompositeFieldMetadataType = | FieldMetadataType.FULL_NAME | FieldMetadataType.LINKS | FieldMetadataType.EMAILS - | FieldMetadataType.PHONES; + | FieldMetadataType.PHONES + | FieldMetadataType.RICH_TEXT_V2; @Injectable() export class CompositeColumnActionFactory extends ColumnActionAbstractFactory { diff --git a/packages/twenty-server/src/engine/metadata-modules/workspace-migration/workspace-migration.factory.ts b/packages/twenty-server/src/engine/metadata-modules/workspace-migration/workspace-migration.factory.ts index 77d7a8351..a231a029a 100644 --- a/packages/twenty-server/src/engine/metadata-modules/workspace-migration/workspace-migration.factory.ts +++ b/packages/twenty-server/src/engine/metadata-modules/workspace-migration/workspace-migration.factory.ts @@ -94,6 +94,10 @@ export class WorkspaceMigrationFactory { FieldMetadataType.TS_VECTOR, { factory: this.tsVectorColumnActionFactory }, ], + [ + FieldMetadataType.RICH_TEXT_V2, + { factory: this.compositeColumnActionFactory }, + ], ]); } diff --git a/packages/twenty-server/src/engine/twenty-orm/utils/get-subfields-for-aggregate-operation.util.ts b/packages/twenty-server/src/engine/twenty-orm/utils/get-subfields-for-aggregate-operation.util.ts index 77bf69665..7ef7d0eff 100644 --- a/packages/twenty-server/src/engine/twenty-orm/utils/get-subfields-for-aggregate-operation.util.ts +++ b/packages/twenty-server/src/engine/twenty-orm/utils/get-subfields-for-aggregate-operation.util.ts @@ -36,6 +36,8 @@ export const getSubfieldsForAggregateOperation = ( 'primaryPhoneCountryCode', 'primaryPhoneCallingCode', ]; + case FieldMetadataType.RICH_TEXT_V2: + return ['blocknote', 'markdown']; default: throw new Error(`Unsupported composite field type: ${fieldType}`); } diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids.ts index 48a5f85bf..5dd57ec4b 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids.ts @@ -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', diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/utils/__tests__/get-ts-vectors-column-expression.utils.spec.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/utils/__tests__/get-ts-vectors-column-expression.utils.spec.ts index 1263cc995..4b9bd0b67 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/utils/__tests__/get-ts-vectors-column-expression.utils.spec.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/utils/__tests__/get-ts-vectors-column-expression.utils.spec.ts @@ -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\", ''))", + ); + }); }); diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/utils/get-ts-vector-column-expression.util.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/utils/get-ts-vector-column-expression.util.ts index d10224b62..2c0eecf07 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/utils/get-ts-vector-column-expression.util.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/utils/get-ts-vector-column-expression.util.ts @@ -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, diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/utils/is-searchable-field.util.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/utils/is-searchable-field.util.ts index c1dd3a3cd..a7d775190 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/utils/is-searchable-field.util.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/utils/is-searchable-field.util.ts @@ -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]; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/utils/is-searchable-subfield.util.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/utils/is-searchable-subfield.util.ts new file mode 100644 index 000000000..e778788fd --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/utils/is-searchable-subfield.util.ts @@ -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; + } +}; diff --git a/packages/twenty-server/src/modules/note/standard-objects/note.workspace-entity.ts b/packages/twenty-server/src/modules/note/standard-objects/note.workspace-entity.ts index 0df8aa8c1..b1f41c45c 100644 --- a/packages/twenty-server/src/modules/note/standard-objects/note.workspace-entity.ts +++ b/packages/twenty-server/src/modules/note/standard-objects/note.workspace-entity.ts @@ -3,11 +3,13 @@ import { FieldMetadataType } from 'twenty-shared'; import { Relation } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/relation.interface'; +import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; import { SEARCH_VECTOR_FIELD } from 'src/engine/metadata-modules/constants/search-vector-field.constants'; import { ActorMetadata, FieldActorSource, } from 'src/engine/metadata-modules/field-metadata/composite-types/actor.composite-type'; +import { RichTextV2Metadata } from 'src/engine/metadata-modules/field-metadata/composite-types/rich-text-v2.composite-type'; import { IndexType } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity'; import { RelationMetadataType, @@ -17,6 +19,7 @@ import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator'; import { WorkspaceFieldIndex } from 'src/engine/twenty-orm/decorators/workspace-field-index.decorator'; import { WorkspaceField } from 'src/engine/twenty-orm/decorators/workspace-field.decorator'; +import { WorkspaceGate } from 'src/engine/twenty-orm/decorators/workspace-gate.decorator'; import { WorkspaceIsNullable } from 'src/engine/twenty-orm/decorators/workspace-is-nullable.decorator'; import { WorkspaceIsSystem } from 'src/engine/twenty-orm/decorators/workspace-is-system.decorator'; import { WorkspaceRelation } from 'src/engine/twenty-orm/decorators/workspace-relation.decorator'; @@ -33,11 +36,9 @@ import { NoteTargetWorkspaceEntity } from 'src/modules/note/standard-objects/not import { TimelineActivityWorkspaceEntity } from 'src/modules/timeline/standard-objects/timeline-activity.workspace-entity'; const TITLE_FIELD_NAME = 'title'; -const BODY_FIELD_NAME = 'body'; export const SEARCH_FIELDS_FOR_NOTES: FieldTypeAndNameMetadata[] = [ { name: TITLE_FIELD_NAME, type: FieldMetadataType.TEXT }, - { name: BODY_FIELD_NAME, type: FieldMetadataType.RICH_TEXT }, ]; @WorkspaceEntity({ @@ -81,6 +82,19 @@ export class NoteWorkspaceEntity extends BaseWorkspaceEntity { @WorkspaceIsNullable() body: string | null; + @WorkspaceField({ + standardId: NOTE_STANDARD_FIELD_IDS.bodyV2, + type: FieldMetadataType.RICH_TEXT_V2, + label: 'Body', + description: 'Note body', + icon: 'IconFilePencil', + }) + @WorkspaceIsNullable() + @WorkspaceGate({ + featureFlag: FeatureFlagKey.IsRichTextV2Enabled, + }) + bodyV2: RichTextV2Metadata | null; + @WorkspaceField({ standardId: NOTE_STANDARD_FIELD_IDS.createdBy, type: FieldMetadataType.ACTOR, diff --git a/packages/twenty-server/src/modules/task/standard-objects/task.workspace-entity.ts b/packages/twenty-server/src/modules/task/standard-objects/task.workspace-entity.ts index 783d42830..4a5cf455f 100644 --- a/packages/twenty-server/src/modules/task/standard-objects/task.workspace-entity.ts +++ b/packages/twenty-server/src/modules/task/standard-objects/task.workspace-entity.ts @@ -8,6 +8,7 @@ import { ActorMetadata, FieldActorSource, } from 'src/engine/metadata-modules/field-metadata/composite-types/actor.composite-type'; +import { RichTextV2Metadata } from 'src/engine/metadata-modules/field-metadata/composite-types/rich-text-v2.composite-type'; import { IndexType } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity'; import { RelationMetadataType, @@ -33,13 +34,13 @@ import { FavoriteWorkspaceEntity } from 'src/modules/favorite/standard-objects/f import { TaskTargetWorkspaceEntity } from 'src/modules/task/standard-objects/task-target.workspace-entity'; import { TimelineActivityWorkspaceEntity } from 'src/modules/timeline/standard-objects/timeline-activity.workspace-entity'; import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity'; +import { WorkspaceGate } from 'src/engine/twenty-orm/decorators/workspace-gate.decorator'; +import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; const TITLE_FIELD_NAME = 'title'; -const BODY_FIELD_NAME = 'body'; export const SEARCH_FIELDS_FOR_TASK: FieldTypeAndNameMetadata[] = [ { name: TITLE_FIELD_NAME, type: FieldMetadataType.TEXT }, - { name: BODY_FIELD_NAME, type: FieldMetadataType.RICH_TEXT }, ]; @WorkspaceEntity({ @@ -83,6 +84,19 @@ export class TaskWorkspaceEntity extends BaseWorkspaceEntity { @WorkspaceIsNullable() body: string | null; + @WorkspaceField({ + standardId: TASK_STANDARD_FIELD_IDS.bodyV2, + type: FieldMetadataType.RICH_TEXT_V2, + label: 'Body', + description: 'Task body', + icon: 'IconFilePencil', + }) + @WorkspaceIsNullable() + @WorkspaceGate({ + featureFlag: FeatureFlagKey.IsRichTextV2Enabled, + }) + bodyV2: RichTextV2Metadata | null; + @WorkspaceField({ standardId: TASK_STANDARD_FIELD_IDS.dueAt, type: FieldMetadataType.DATE_TIME, diff --git a/packages/twenty-shared/src/types/FieldMetadataType.ts b/packages/twenty-shared/src/types/FieldMetadataType.ts index 83589ce92..686da7e67 100644 --- a/packages/twenty-shared/src/types/FieldMetadataType.ts +++ b/packages/twenty-shared/src/types/FieldMetadataType.ts @@ -18,6 +18,7 @@ export enum FieldMetadataType { POSITION = 'POSITION', ADDRESS = 'ADDRESS', RAW_JSON = 'RAW_JSON', + RICH_TEXT_V2 = 'RICH_TEXT_V2', RICH_TEXT = 'RICH_TEXT', ACTOR = 'ACTOR', ARRAY = 'ARRAY', diff --git a/yarn.lock b/yarn.lock index 3729e0e8a..1ec17702b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3415,6 +3415,55 @@ __metadata: languageName: node linkType: hard +"@blocknote/core@npm:^0.17.1": + version: 0.17.1 + resolution: "@blocknote/core@npm:0.17.1" + dependencies: + "@emoji-mart/data": "npm:^1.2.1" + "@tiptap/core": "npm:^2.7.1" + "@tiptap/extension-bold": "npm:^2.7.1" + "@tiptap/extension-code": "npm:^2.7.1" + "@tiptap/extension-collaboration": "npm:^2.7.1" + "@tiptap/extension-collaboration-cursor": "npm:^2.7.1" + "@tiptap/extension-dropcursor": "npm:^2.7.1" + "@tiptap/extension-gapcursor": "npm:^2.7.1" + "@tiptap/extension-hard-break": "npm:^2.7.1" + "@tiptap/extension-history": "npm:^2.7.1" + "@tiptap/extension-horizontal-rule": "npm:^2.7.1" + "@tiptap/extension-italic": "npm:^2.7.1" + "@tiptap/extension-link": "npm:^2.7.1" + "@tiptap/extension-paragraph": "npm:^2.7.1" + "@tiptap/extension-strike": "npm:^2.7.1" + "@tiptap/extension-table-cell": "npm:^2.7.1" + "@tiptap/extension-table-header": "npm:^2.7.1" + "@tiptap/extension-table-row": "npm:^2.7.1" + "@tiptap/extension-text": "npm:^2.7.1" + "@tiptap/extension-underline": "npm:^2.7.1" + "@tiptap/pm": "npm:^2.7.1" + emoji-mart: "npm:^5.6.0" + hast-util-from-dom: "npm:^4.2.0" + prosemirror-model: "npm:^1.21.0" + prosemirror-state: "npm:^1.4.3" + prosemirror-tables: "npm:^1.3.7" + prosemirror-transform: "npm:^1.9.0" + prosemirror-view: "npm:^1.33.7" + rehype-format: "npm:^5.0.0" + rehype-parse: "npm:^8.0.4" + rehype-remark: "npm:^9.1.2" + rehype-stringify: "npm:^9.0.3" + remark-gfm: "npm:^3.0.1" + remark-parse: "npm:^10.0.1" + remark-rehype: "npm:^10.1.0" + remark-stringify: "npm:^10.0.2" + unified: "npm:^10.1.2" + uuid: "npm:^8.3.2" + y-prosemirror: "npm:1.2.12" + y-protocols: "npm:^1.0.6" + yjs: "npm:^13.6.15" + checksum: 10c0/0acd1a099832d8e271983924f19e59aa056ead278a8bac8ab7a64d6c7d40a787a27143bdee3d6b6f3dfa26c7723207d98d7124ffef8ff9c4cfdf3034140716ab + languageName: node + linkType: hard + "@blocknote/core@npm:^0.22.0": version: 0.22.0 resolution: "@blocknote/core@npm:0.22.0" @@ -3483,6 +3532,25 @@ __metadata: languageName: node linkType: hard +"@blocknote/react@npm:^0.17.1": + version: 0.17.1 + resolution: "@blocknote/react@npm:0.17.1" + dependencies: + "@blocknote/core": "npm:^0.17.1" + "@floating-ui/react": "npm:^0.26.4" + "@tiptap/core": "npm:^2.7.1" + "@tiptap/react": "npm:^2.7.1" + lodash.merge: "npm:^4.6.2" + react: "npm:^18" + react-dom: "npm:^18" + react-icons: "npm:^5.2.1" + peerDependencies: + react: ^18 + react-dom: ^18 + checksum: 10c0/4914dce225f60905b3dfe59805d7cf1f0c0c6d87295a09204333b913f6449cc90c2947baeb65c1f06106beaea8b50ac5c2785782cb2528f7e913b2877427c3e4 + languageName: node + linkType: hard + "@blocknote/react@npm:^0.22.0": version: 0.22.0 resolution: "@blocknote/react@npm:0.22.0" @@ -3500,6 +3568,27 @@ __metadata: languageName: node linkType: hard +"@blocknote/server-util@npm:0.17.1": + version: 0.17.1 + resolution: "@blocknote/server-util@npm:0.17.1" + dependencies: + "@blocknote/core": "npm:^0.17.1" + "@blocknote/react": "npm:^0.17.1" + "@tiptap/core": "npm:^2.7.1" + "@tiptap/pm": "npm:^2.7.1" + jsdom: "npm:^21.1.0" + react: "npm:^18" + react-dom: "npm:^18" + y-prosemirror: "npm:1.2.12" + y-protocols: "npm:^1.0.6" + yjs: "npm:^13.6.15" + peerDependencies: + react: ^18 + react-dom: ^18 + checksum: 10c0/7d400dbf19562f8827bc524f87d673d711fba95a50fb299e0eb638f01c2dc87fd840a132b33dae60c0944637208f18a632f72f7664cb03b8ce81f5be7f8e59f0 + languageName: node + linkType: hard + "@blocknote/xl-docx-exporter@npm:^0.22.0": version: 0.22.0 resolution: "@blocknote/xl-docx-exporter@npm:0.22.0" @@ -15688,6 +15777,16 @@ __metadata: languageName: node linkType: hard +"@tiptap/extension-dropcursor@npm:^2.7.1": + version: 2.11.0 + resolution: "@tiptap/extension-dropcursor@npm:2.11.0" + peerDependencies: + "@tiptap/core": ^2.7.0 + "@tiptap/pm": ^2.7.0 + checksum: 10c0/12ace987deec4bd02f52ee7a8f837bd71d560bca1ce670d43c6a715526a336aa5431ed044cba44babd45f7f0ed79002d16f03430ce72899a4a9713679e924717 + languageName: node + linkType: hard + "@tiptap/extension-floating-menu@npm:^2.10.4": version: 2.10.4 resolution: "@tiptap/extension-floating-menu@npm:2.10.4" @@ -32934,6 +33033,45 @@ __metadata: languageName: node linkType: hard +"jsdom@npm:^21.1.0": + version: 21.1.2 + resolution: "jsdom@npm:21.1.2" + dependencies: + abab: "npm:^2.0.6" + acorn: "npm:^8.8.2" + acorn-globals: "npm:^7.0.0" + cssstyle: "npm:^3.0.0" + data-urls: "npm:^4.0.0" + decimal.js: "npm:^10.4.3" + domexception: "npm:^4.0.0" + escodegen: "npm:^2.0.0" + form-data: "npm:^4.0.0" + html-encoding-sniffer: "npm:^3.0.0" + http-proxy-agent: "npm:^5.0.0" + https-proxy-agent: "npm:^5.0.1" + is-potential-custom-element-name: "npm:^1.0.1" + nwsapi: "npm:^2.2.4" + parse5: "npm:^7.1.2" + rrweb-cssom: "npm:^0.6.0" + saxes: "npm:^6.0.0" + symbol-tree: "npm:^3.2.4" + tough-cookie: "npm:^4.1.2" + w3c-xmlserializer: "npm:^4.0.0" + webidl-conversions: "npm:^7.0.0" + whatwg-encoding: "npm:^2.0.0" + whatwg-mimetype: "npm:^3.0.0" + whatwg-url: "npm:^12.0.1" + ws: "npm:^8.13.0" + xml-name-validator: "npm:^4.0.0" + peerDependencies: + canvas: ^2.5.0 + peerDependenciesMeta: + canvas: + optional: true + checksum: 10c0/905012680891fa0c92b8c18acfa35fc0b3e4b15f778ee3494aec1aca3274875160d2be35917d666b8eacd0b3121f483bd95fbe35e14790a004b805b1cf01818a + languageName: node + linkType: hard + "jsdom@npm:~22.1.0": version: 22.1.0 resolution: "jsdom@npm:22.1.0" @@ -40128,7 +40266,7 @@ __metadata: languageName: node linkType: hard -"prosemirror-tables@npm:^1.6.1": +"prosemirror-tables@npm:^1.3.7, prosemirror-tables@npm:^1.6.1": version: 1.6.2 resolution: "prosemirror-tables@npm:1.6.2" dependencies: @@ -40164,7 +40302,7 @@ __metadata: languageName: node linkType: hard -"prosemirror-transform@npm:^1.10.2": +"prosemirror-transform@npm:^1.10.2, prosemirror-transform@npm:^1.9.0": version: 1.10.2 resolution: "prosemirror-transform@npm:1.10.2" dependencies: @@ -40649,7 +40787,7 @@ __metadata: languageName: node linkType: hard -"react-dom@npm:^18.2.0": +"react-dom@npm:^18, react-dom@npm:^18.2.0": version: 18.3.1 resolution: "react-dom@npm:18.3.1" dependencies: @@ -45818,6 +45956,7 @@ __metadata: "@babel/preset-typescript": "npm:^7.24.6" "@blocknote/mantine": "npm:^0.22.0" "@blocknote/react": "npm:^0.22.0" + "@blocknote/server-util": "npm:0.17.1" "@codesandbox/sandpack-react": "npm:^2.13.5" "@crxjs/vite-plugin": "npm:^1.0.14" "@dagrejs/dagre": "npm:^1.1.2" @@ -48799,6 +48938,21 @@ __metadata: languageName: node linkType: hard +"y-prosemirror@npm:1.2.12": + version: 1.2.12 + resolution: "y-prosemirror@npm:1.2.12" + dependencies: + lib0: "npm:^0.2.42" + peerDependencies: + prosemirror-model: ^1.7.1 + prosemirror-state: ^1.2.3 + prosemirror-view: ^1.9.10 + y-protocols: ^1.0.1 + yjs: ^13.5.38 + checksum: 10c0/c460aa9104c71806112a17b52449221343095c774bc929a3bcfaa6d752ce9af1a5a8359c974625c70de8bf48e10b2aa8702f12ca2027f85c6097d1621969beeb + languageName: node + linkType: hard + "y-prosemirror@npm:1.2.13": version: 1.2.13 resolution: "y-prosemirror@npm:1.2.13"