diff --git a/package.json b/package.json index f9b0f86ca8d3..be9bd8099495 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 61b8ce087c81..f0cddd33099d 100644 --- a/packages/twenty-front/src/generated-metadata/graphql.ts +++ b/packages/twenty-front/src/generated-metadata/graphql.ts @@ -406,6 +406,7 @@ export enum FeatureFlagKey { IsStripeIntegrationEnabled = 'IsStripeIntegrationEnabled', IsUniqueIndexesEnabled = 'IsUniqueIndexesEnabled', IsViewGroupsEnabled = 'IsViewGroupsEnabled', + IsRichTextV2Enabled = 'IsRichTextV2Enabled', IsWorkflowEnabled = 'IsWorkflowEnabled' } @@ -448,6 +449,7 @@ export enum FieldMetadataType { RawJson = 'RAW_JSON', Relation = 'RELATION', RichText = 'RICH_TEXT', + RichTextV2 = 'RICH_TEXT_V2', Select = 'SELECT', Text = 'TEXT', TsVector = 'TS_VECTOR', diff --git a/packages/twenty-front/src/generated/graphql.tsx b/packages/twenty-front/src/generated/graphql.tsx index a878cba313c5..1f773fc10441 100644 --- a/packages/twenty-front/src/generated/graphql.tsx +++ b/packages/twenty-front/src/generated/graphql.tsx @@ -334,6 +334,7 @@ export enum FeatureFlagKey { IsJsonFilterEnabled = 'IsJsonFilterEnabled', IsMicrosoftSyncEnabled = 'IsMicrosoftSyncEnabled', IsPostgreSqlIntegrationEnabled = 'IsPostgreSQLIntegrationEnabled', + IsRichTextV2Enabled = 'IsRichTextV2Enabled', IsStripeIntegrationEnabled = 'IsStripeIntegrationEnabled', IsUniqueIndexesEnabled = 'IsUniqueIndexesEnabled', IsViewGroupsEnabled = 'IsViewGroupsEnabled', @@ -379,6 +380,7 @@ export enum FieldMetadataType { RawJson = 'RAW_JSON', Relation = 'RELATION', RichText = 'RICH_TEXT', + RichTextV2 = 'RICH_TEXT_V2', Select = 'SELECT', Text = 'TEXT', TsVector = 'TS_VECTOR', diff --git a/packages/twenty-front/src/modules/activities/components/ActivityRichTextEditor.tsx b/packages/twenty-front/src/modules/activities/components/ActivityRichTextEditor.tsx index 4dab9fa2fc60..71c31f41c8ef 100644 --- a/packages/twenty-front/src/modules/activities/components/ActivityRichTextEditor.tsx +++ b/packages/twenty-front/src/modules/activities/components/ActivityRichTextEditor.tsx @@ -44,6 +44,10 @@ export const ActivityRichTextEditor = ({ activityId, activityObjectNameSingular, }: ActivityRichTextEditorProps) => { + const isRichTextV2Enabled = useIsFeatureEnabled( + FeatureFlagKey.IsRichTextV2Enabled, + ); + const [activityInStore] = useRecoilState(recordStoreFamilyState(activityId)); const cache = useApolloClient().cache; @@ -67,12 +71,19 @@ export const ActivityRichTextEditor = ({ activityObjectNameSingular: activityObjectNameSingular, }); - const persistBodyDebounced = useDebouncedCallback((newBody: string) => { + const persistBodyDebounced = useDebouncedCallback((blocknote: string) => { + const body = isRichTextV2Enabled + ? { + blocknote, + markdown: null, + } + : (blocknote as any); + if (isDefined(activity)) { upsertActivity({ activity, input: { - body: newBody, + body, }, }); } @@ -163,14 +174,18 @@ export const ActivityRichTextEditor = ({ }; const initialBody = useMemo(() => { + const blocknote = isRichTextV2Enabled + ? activity?.body.blocknote + : activity?.body; + if ( isDefined(activity) && - isNonEmptyString(activity.body) && - activity?.body !== '{}' + isNonEmptyString(blocknote) && + blocknote !== '{}' ) { - return JSON.parse(activity.body); + return JSON.parse(blocknote); } - }, [activity]); + }, [activity, isRichTextV2Enabled]); const handleEditorBuiltInUploadFile = async (file: File) => { const { attachmentAbsoluteURL } = await handleUploadAttachment(file); diff --git a/packages/twenty-front/src/modules/activities/hooks/__tests__/useActivityTargetObjectRecords.test.tsx b/packages/twenty-front/src/modules/activities/hooks/__tests__/useActivityTargetObjectRecords.test.tsx index 7d1426f4cb75..96c2e918895f 100644 --- a/packages/twenty-front/src/modules/activities/hooks/__tests__/useActivityTargetObjectRecords.test.tsx +++ b/packages/twenty-front/src/modules/activities/hooks/__tests__/useActivityTargetObjectRecords.test.tsx @@ -94,7 +94,10 @@ const task = { createdAt: '2023-04-26T10:12:42.33625+00:00', updatedAt: '2023-04-26T10:23:42.33625+00:00', title: 'Task title', - body: null, + body: { + blocknote: null, + markdown: null, + }, assigneeId: null, status: null, dueAt: '2023-04-26T10:12:42.33625+00:00', diff --git a/packages/twenty-front/src/modules/activities/notes/components/NoteCard.tsx b/packages/twenty-front/src/modules/activities/notes/components/NoteCard.tsx index 22879b117a0c..8ed440e393d8 100644 --- a/packages/twenty-front/src/modules/activities/notes/components/NoteCard.tsx +++ b/packages/twenty-front/src/modules/activities/notes/components/NoteCard.tsx @@ -71,7 +71,7 @@ export const NoteCard = ({ const openActivityRightDrawer = useOpenActivityRightDrawer({ objectNameSingular: CoreObjectNameSingular.Note, }); - const body = getActivityPreview(note.body); + const body = getActivityPreview(note.body.blocknote); const { FieldContextProvider: NoteTargetsContextProvider } = useFieldContext({ objectNameSingular: CoreObjectNameSingular.Note, diff --git a/packages/twenty-front/src/modules/activities/tasks/components/TaskRow.tsx b/packages/twenty-front/src/modules/activities/tasks/components/TaskRow.tsx index 90e11ab06448..3049dd24f1fe 100644 --- a/packages/twenty-front/src/modules/activities/tasks/components/TaskRow.tsx +++ b/packages/twenty-front/src/modules/activities/tasks/components/TaskRow.tsx @@ -82,7 +82,7 @@ export const TaskRow = ({ task }: { task: Task }) => { objectNameSingular: CoreObjectNameSingular.Task, }); - const body = getActivitySummary(task.body); + const body = getActivitySummary(task.body.blocknote); const { completeTask } = useCompleteTask(task); const { FieldContextProvider: TaskTargetsContextProvider } = useFieldContext({ diff --git a/packages/twenty-front/src/modules/activities/tasks/hooks/__tests__/useCompleteTask.test.tsx b/packages/twenty-front/src/modules/activities/tasks/hooks/__tests__/useCompleteTask.test.tsx index 51408dac5983..a06dcc35c21d 100644 --- a/packages/twenty-front/src/modules/activities/tasks/hooks/__tests__/useCompleteTask.test.tsx +++ b/packages/twenty-front/src/modules/activities/tasks/hooks/__tests__/useCompleteTask.test.tsx @@ -10,7 +10,10 @@ const task: Task = { id: '123', status: 'DONE', title: 'Test', - body: 'Test', + body: { + blocknote: 'Test', + markdown: 'Test', + }, dueAt: '2024-03-15T07:33:14.212Z', createdAt: '2024-03-15T07:33:14.212Z', updatedAt: '2024-03-15T07:33:14.212Z', diff --git a/packages/twenty-front/src/modules/activities/types/Activity.ts b/packages/twenty-front/src/modules/activities/types/Activity.ts index b54fd02a2b30..62d0448400a2 100644 --- a/packages/twenty-front/src/modules/activities/types/Activity.ts +++ b/packages/twenty-front/src/modules/activities/types/Activity.ts @@ -3,5 +3,8 @@ export type Activity = { createdAt: string; updatedAt: string; title: string; - body: string | null; + body: { + blocknote: string | null; + markdown: string | null; + }; }; diff --git a/packages/twenty-front/src/modules/command-menu/hooks/useCommandMenuCommands.tsx b/packages/twenty-front/src/modules/command-menu/hooks/useCommandMenuCommands.tsx index 6504bb5c256d..803b6b27d565 100644 --- a/packages/twenty-front/src/modules/command-menu/hooks/useCommandMenuCommands.tsx +++ b/packages/twenty-front/src/modules/command-menu/hooks/useCommandMenuCommands.tsx @@ -35,6 +35,9 @@ import { FeatureFlagKey } from '~/generated/graphql'; import { getLogoUrlFromDomainName } from '~/utils'; export const useCommandMenuCommands = () => { + const isRichTextV2Enabled = useIsFeatureEnabled( + FeatureFlagKey.IsRichTextV2Enabled, + ); const actionMenuEntries = useRecoilComponentValueV2( actionMenuEntriesComponentSelector, ); @@ -146,7 +149,13 @@ export const useCommandMenuCommands = () => { filter: deferredCommandMenuSearch ? makeOrFilterVariables([ { title: { ilike: `%${deferredCommandMenuSearch}%` } }, - { body: { ilike: `%${deferredCommandMenuSearch}%` } }, + { + body: isRichTextV2Enabled + ? { + blocknote: { ilike: `%${deferredCommandMenuSearch}%` }, + } + : { ilike: `%${deferredCommandMenuSearch}%` }, + }, ]) : undefined, limit: 3, @@ -158,7 +167,13 @@ export const useCommandMenuCommands = () => { filter: deferredCommandMenuSearch ? makeOrFilterVariables([ { title: { ilike: `%${deferredCommandMenuSearch}%` } }, - { body: { ilike: `%${deferredCommandMenuSearch}%` } }, + { + body: isRichTextV2Enabled + ? { + blocknote: { ilike: `%${deferredCommandMenuSearch}%` }, + } + : { ilike: `%${deferredCommandMenuSearch}%` }, + }, ]) : undefined, limit: 3, diff --git a/packages/twenty-front/src/modules/object-metadata/utils/mapFieldMetadataToGraphQLQuery.ts b/packages/twenty-front/src/modules/object-metadata/utils/mapFieldMetadataToGraphQLQuery.ts index 4cd8dbcbb009..3301a9038282 100644 --- a/packages/twenty-front/src/modules/object-metadata/utils/mapFieldMetadataToGraphQLQuery.ts +++ b/packages/twenty-front/src/modules/object-metadata/utils/mapFieldMetadataToGraphQLQuery.ts @@ -162,5 +162,13 @@ ${mapObjectMetadataToGraphQLQuery({ }`; } + if (fieldType === FieldMetadataType.RichTextV2) { + return `${field.name} +{ + blocknote + markdown +}`; + } + return ''; }; diff --git a/packages/twenty-front/src/modules/object-record/graphql/types/RecordGqlOperationFilter.ts b/packages/twenty-front/src/modules/object-record/graphql/types/RecordGqlOperationFilter.ts index 1d4fb29bc991..d1fa4826869d 100644 --- a/packages/twenty-front/src/modules/object-record/graphql/types/RecordGqlOperationFilter.ts +++ b/packages/twenty-front/src/modules/object-record/graphql/types/RecordGqlOperationFilter.ts @@ -129,6 +129,15 @@ export type RawJsonFilter = { is?: IsFilter; }; +export type RichTextV2LeafFilter = { + ilike?: string; +}; + +export type RichTextV2Filter = { + blocknote?: RichTextV2LeafFilter; + markdown?: RichTextV2LeafFilter; +}; + export type LeafFilter = | UUIDFilter | StringFilter @@ -144,6 +153,7 @@ export type LeafFilter = | PhonesFilter | ArrayFilter | RawJsonFilter + | RichTextV2Filter | undefined; export type AndObjectRecordFilter = { diff --git a/packages/twenty-front/src/modules/object-record/record-field/components/FieldDisplay.tsx b/packages/twenty-front/src/modules/object-record/record-field/components/FieldDisplay.tsx index 41740a1a4191..0b0c5d287be8 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/components/FieldDisplay.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/components/FieldDisplay.tsx @@ -9,6 +9,7 @@ import { PhonesFieldDisplay } from '@/object-record/record-field/meta-types/disp import { RatingFieldDisplay } from '@/object-record/record-field/meta-types/display/components/RatingFieldDisplay'; import { RelationFromManyFieldDisplay } from '@/object-record/record-field/meta-types/display/components/RelationFromManyFieldDisplay'; import { RichTextFieldDisplay } from '@/object-record/record-field/meta-types/display/components/RichTextFieldDisplay'; +import { RichTextV2FieldDisplay } from '@/object-record/record-field/meta-types/display/components/RichTextV2FieldDisplay'; import { isFieldIdentifierDisplay } from '@/object-record/record-field/meta-types/display/utils/isFieldIdentifierDisplay'; import { isFieldActor } from '@/object-record/record-field/types/guards/isFieldActor'; import { isFieldArray } from '@/object-record/record-field/types/guards/isFieldArray'; @@ -20,6 +21,7 @@ import { isFieldRating } from '@/object-record/record-field/types/guards/isField import { isFieldRelationFromManyObjects } from '@/object-record/record-field/types/guards/isFieldRelationFromManyObjects'; import { isFieldRelationToOneObject } from '@/object-record/record-field/types/guards/isFieldRelationToOneObject'; import { isFieldRichText } from '@/object-record/record-field/types/guards/isFieldRichText'; +import { isFieldRichTextV2 } from '@/object-record/record-field/types/guards/isFieldRichTextV2'; import { FieldContext } from '../contexts/FieldContext'; import { AddressFieldDisplay } from '../meta-types/display/components/AddressFieldDisplay'; import { ChipFieldDisplay } from '../meta-types/display/components/ChipFieldDisplay'; @@ -90,6 +92,8 @@ export const FieldDisplay = () => { ) : isFieldRichText(fieldDefinition) ? ( + ) : isFieldRichTextV2(fieldDefinition) ? ( + ) : isFieldActor(fieldDefinition) ? ( ) : isFieldArray(fieldDefinition) ? ( diff --git a/packages/twenty-front/src/modules/object-record/record-field/hooks/usePersistField.ts b/packages/twenty-front/src/modules/object-record/record-field/hooks/usePersistField.ts index ecb9711af7d2..2df2916301ac 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/hooks/usePersistField.ts +++ b/packages/twenty-front/src/modules/object-record/record-field/hooks/usePersistField.ts @@ -28,8 +28,8 @@ import { RecordForSelect } from '@/object-record/relation-picker/types/RecordFor import { isFieldArray } from '@/object-record/record-field/types/guards/isFieldArray'; import { isFieldArrayValue } from '@/object-record/record-field/types/guards/isFieldArrayValue'; -import { isFieldRichText } from '@/object-record/record-field/types/guards/isFieldRichText'; -import { isFieldRichTextValue } from '@/object-record/record-field/types/guards/isFieldRichTextValue'; +import { isFieldRichTextV2 } from '@/object-record/record-field/types/guards/isFieldRichTextV2'; +import { isFieldRichTextV2Value } from '@/object-record/record-field/types/guards/isFieldRichTextValueV2'; import { FieldContext } from '../contexts/FieldContext'; import { isFieldBoolean } from '../types/guards/isFieldBoolean'; import { isFieldBooleanValue } from '../types/guards/isFieldBooleanValue'; @@ -114,8 +114,8 @@ export const usePersistField = () => { isFieldRawJsonValue(valueToPersist); const fieldIsRichText = - isFieldRichText(fieldDefinition) && - isFieldRichTextValue(valueToPersist); + isFieldRichTextV2(fieldDefinition) && + isFieldRichTextV2Value(valueToPersist); const fieldIsArray = isFieldArray(fieldDefinition) && isFieldArrayValue(valueToPersist); diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/RichTextV2FieldDisplay.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/RichTextV2FieldDisplay.tsx new file mode 100644 index 000000000000..d04f739d985f --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/RichTextV2FieldDisplay.tsx @@ -0,0 +1,16 @@ +import { useRichTextV2FieldDisplay } from '@/object-record/record-field/meta-types/hooks/useRichTextV2FieldDisplay'; +import { getFirstNonEmptyLineOfRichText } from '@/ui/input/editor/utils/getFirstNonEmptyLineOfRichText'; +import { PartialBlock } from '@blocknote/core'; +import { parseJson } from '~/utils/parseJson'; + +export const RichTextV2FieldDisplay = () => { + const { fieldValue } = useRichTextV2FieldDisplay(); + + const blocks = parseJson(fieldValue?.blocknote); + + return ( +
+ {getFirstNonEmptyLineOfRichText(blocks)} +
+ ); +}; diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useRichTextV2Field.ts b/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useRichTextV2Field.ts new file mode 100644 index 000000000000..3fa8cedf5472 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useRichTextV2Field.ts @@ -0,0 +1,73 @@ +import { useContext } from 'react'; +import { useRecoilState, useRecoilValue } from 'recoil'; + +import { useRecordFieldInput } from '@/object-record/record-field/hooks/useRecordFieldInput'; +import { + FieldRichTextV2Value, + FieldRichTextValue, +} from '@/object-record/record-field/types/FieldMetadata'; +import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector'; +import { FieldMetadataType } from '~/generated-metadata/graphql'; + +import { usePersistField } from '@/object-record/record-field/hooks/usePersistField'; +import { isFieldRichTextV2 } from '@/object-record/record-field/types/guards/isFieldRichTextV2'; +import { isFieldRichTextV2Value } from '@/object-record/record-field/types/guards/isFieldRichTextValueV2'; +import { PartialBlock } from '@blocknote/core'; +import { isNonEmptyString } from '@sniptt/guards'; +import { FieldContext } from '../../contexts/FieldContext'; +import { assertFieldMetadata } from '../../types/guards/assertFieldMetadata'; + +export const useRichTextV2Field = () => { + const { recordId, fieldDefinition, hotkeyScope, maxWidth } = + useContext(FieldContext); + + assertFieldMetadata( + FieldMetadataType.RichTextV2, + isFieldRichTextV2, + fieldDefinition, + ); + + const fieldName = fieldDefinition.metadata.fieldName; + + const [fieldValue, setFieldValue] = useRecoilState( + recordStoreFamilySelector({ + recordId, + fieldName: fieldName, + }), + ); + const fieldRichTextV2Value = isFieldRichTextV2Value(fieldValue) + ? fieldValue + : ({ blocknote: null, markdown: null } as FieldRichTextV2Value); + + const { setDraftValue, getDraftValueSelector } = + useRecordFieldInput(`${recordId}-${fieldName}`); + + const draftValue = useRecoilValue(getDraftValueSelector()); + + const draftValueParsed: PartialBlock[] = isNonEmptyString(draftValue) + ? JSON.parse(draftValue) + : draftValue; + + const persistField = usePersistField(); + + const persistRichTextField = (nextValue: PartialBlock[]) => { + if (!nextValue) { + persistField(null); + } else { + const parsedValueToPersist = JSON.stringify(nextValue); + + persistField(parsedValueToPersist); + } + }; + + return { + draftValue: draftValueParsed, + setDraftValue, + maxWidth, + fieldDefinition, + fieldValue: fieldRichTextV2Value, + setFieldValue, + hotkeyScope, + persistRichTextField, + }; +}; diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useRichTextV2FieldDisplay.ts b/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useRichTextV2FieldDisplay.ts new file mode 100644 index 000000000000..8b33e1c639c2 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useRichTextV2FieldDisplay.ts @@ -0,0 +1,32 @@ +import { useContext } from 'react'; + +import { useRecordFieldValue } from '@/object-record/record-store/contexts/RecordFieldValueSelectorContext'; + +import { FieldRichTextV2Value } from '@/object-record/record-field/types/FieldMetadata'; +import { assertFieldMetadata } from '@/object-record/record-field/types/guards/assertFieldMetadata'; +import { isFieldRichTextV2 } from '@/object-record/record-field/types/guards/isFieldRichTextV2'; +import { FieldMetadataType } from '~/generated-metadata/graphql'; +import { FieldContext } from '../../contexts/FieldContext'; + +export const useRichTextV2FieldDisplay = () => { + const { recordId, fieldDefinition, hotkeyScope } = useContext(FieldContext); + + assertFieldMetadata( + FieldMetadataType.RichTextV2, + isFieldRichTextV2, + fieldDefinition, + ); + + const fieldName = fieldDefinition.metadata.fieldName; + + const fieldValue = useRecordFieldValue( + recordId, + fieldName, + ); + + return { + fieldDefinition, + fieldValue, + hotkeyScope, + }; +}; 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 c04cca016008..0207f24123c4 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 @@ -127,6 +127,12 @@ export type FieldRawJsonMetadata = { settings?: null; }; +export type FieldRichTextV2Metadata = { + objectMetadataNameSingular?: string; + fieldName: string; + settings?: null; +}; + export type FieldRichTextMetadata = { objectMetadataNameSingular?: string; fieldName: string; @@ -210,7 +216,9 @@ export type FieldMetadata = | FieldAddressMetadata | FieldActorMetadata | FieldArrayMetadata - | FieldTsVectorMetadata; + | FieldTsVectorMetadata + | FieldRichTextV2Metadata + | FieldRichTextMetadata; export type FieldTextValue = string; export type FieldUUidValue = string; // TODO: can we replace with a template literal type, or maybe overkill ? @@ -262,6 +270,11 @@ export type FieldRelationValue< export type Json = ZodHelperLiteral | { [key: string]: Json } | Json[]; export type FieldJsonValue = Record | Json[] | null; +export type FieldRichTextV2Value = { + blocknote: string | null; + markdown: string | null; +}; + export type FieldRichTextValue = null | string; export type FieldActorValue = { diff --git a/packages/twenty-front/src/modules/object-record/record-field/types/guards/assertFieldMetadata.ts b/packages/twenty-front/src/modules/object-record/record-field/types/guards/assertFieldMetadata.ts index 70640e39314e..45367c966386 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/types/guards/assertFieldMetadata.ts +++ b/packages/twenty-front/src/modules/object-record/record-field/types/guards/assertFieldMetadata.ts @@ -23,6 +23,7 @@ import { FieldRawJsonMetadata, FieldRelationMetadata, FieldRichTextMetadata, + FieldRichTextV2Metadata, FieldSelectMetadata, FieldTextMetadata, FieldUuidMetadata, @@ -68,15 +69,17 @@ type AssertFieldMetadataFunction = < ? FieldAddressMetadata : E extends 'RAW_JSON' ? FieldRawJsonMetadata - : E extends 'RICH_TEXT' - ? FieldRichTextMetadata - : E extends 'ACTOR' - ? FieldActorMetadata - : E extends 'ARRAY' - ? FieldArrayMetadata - : E extends 'PHONES' - ? FieldPhonesMetadata - : never, + : E extends 'RICH_TEXT_V2' + ? FieldRichTextV2Metadata + : E extends 'RICH_TEXT' + ? FieldRichTextMetadata + : E extends 'ACTOR' + ? FieldActorMetadata + : E extends 'ARRAY' + ? FieldArrayMetadata + : E extends 'PHONES' + ? FieldPhonesMetadata + : never, >( fieldType: E, fieldTypeGuard: ( diff --git a/packages/twenty-front/src/modules/object-record/record-field/types/guards/isFieldRichTextV2.ts b/packages/twenty-front/src/modules/object-record/record-field/types/guards/isFieldRichTextV2.ts new file mode 100644 index 000000000000..c887cc1173cf --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-field/types/guards/isFieldRichTextV2.ts @@ -0,0 +1,9 @@ +import { FieldMetadataType } from '~/generated-metadata/graphql'; + +import { FieldDefinition } from '../FieldDefinition'; +import { FieldMetadata, FieldRichTextV2Metadata } from '../FieldMetadata'; + +export const isFieldRichTextV2 = ( + field: Pick, 'type'>, +): field is FieldDefinition => + field.type === FieldMetadataType.RichTextV2; diff --git a/packages/twenty-front/src/modules/object-record/record-field/types/guards/isFieldRichTextValue.ts b/packages/twenty-front/src/modules/object-record/record-field/types/guards/isFieldRichTextValue.ts index 0b1645644f55..d2fc793fd6d2 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/types/guards/isFieldRichTextValue.ts +++ b/packages/twenty-front/src/modules/object-record/record-field/types/guards/isFieldRichTextValue.ts @@ -2,7 +2,7 @@ import { z } from 'zod'; import { FieldRichTextValue } from '../FieldMetadata'; export const richTextSchema: z.ZodType = z.union([ - z.null(), // Exclude literal values other than null + z.null(), z.string(), ]); diff --git a/packages/twenty-front/src/modules/object-record/record-field/types/guards/isFieldRichTextValueV2.ts b/packages/twenty-front/src/modules/object-record/record-field/types/guards/isFieldRichTextValueV2.ts new file mode 100644 index 000000000000..b163d008b99d --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-field/types/guards/isFieldRichTextValueV2.ts @@ -0,0 +1,12 @@ +import { FieldRichTextV2Value } from '@/object-record/record-field/types/FieldMetadata'; +import { z } from 'zod'; + +export const richTextV2Schema: z.ZodType = z.object({ + blocknote: z.string().nullable(), + markdown: z.string().nullable(), +}); + +export const isFieldRichTextV2Value = ( + fieldValue: unknown, +): fieldValue is FieldRichTextV2Value => + richTextV2Schema.safeParse(fieldValue).success; diff --git a/packages/twenty-front/src/modules/object-record/record-field/utils/isFieldValueEmpty.ts b/packages/twenty-front/src/modules/object-record/record-field/utils/isFieldValueEmpty.ts index e8e3ebe3fb86..d2a01d55c3c7 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/utils/isFieldValueEmpty.ts +++ b/packages/twenty-front/src/modules/object-record/record-field/utils/isFieldValueEmpty.ts @@ -29,6 +29,8 @@ import { isFieldRating } from '@/object-record/record-field/types/guards/isField import { isFieldRawJson } from '@/object-record/record-field/types/guards/isFieldRawJson'; import { isFieldRelation } from '@/object-record/record-field/types/guards/isFieldRelation'; import { isFieldRichText } from '@/object-record/record-field/types/guards/isFieldRichText'; +import { isFieldRichTextV2 } from '@/object-record/record-field/types/guards/isFieldRichTextV2'; +import { isFieldRichTextV2Value } from '@/object-record/record-field/types/guards/isFieldRichTextValueV2'; import { isFieldSelect } from '@/object-record/record-field/types/guards/isFieldSelect'; import { isFieldSelectValue } from '@/object-record/record-field/types/guards/isFieldSelectValue'; import { isFieldText } from '@/object-record/record-field/types/guards/isFieldText'; @@ -142,6 +144,14 @@ export const isFieldValueEmpty = ({ return false; } + if (isFieldRichTextV2(fieldDefinition)) { + return ( + !isFieldRichTextV2Value(fieldValue) || + (isValueEmpty(fieldValue?.blocknote) && + isValueEmpty(fieldValue?.markdown)) + ); + } + throw new Error( `Entity field type not supported in isFieldValueEmpty : ${fieldDefinition.type}}`, ); diff --git a/packages/twenty-front/src/modules/object-record/record-field/utils/isFieldValueReadOnly.ts b/packages/twenty-front/src/modules/object-record/record-field/utils/isFieldValueReadOnly.ts index e3e59574d385..bf85caa35a24 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/utils/isFieldValueReadOnly.ts +++ b/packages/twenty-front/src/modules/object-record/record-field/utils/isFieldValueReadOnly.ts @@ -2,6 +2,8 @@ import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSi import { isWorkflowSubObjectMetadata } from '@/object-metadata/utils/isWorkflowSubObjectMetadata'; import { isFieldActor } from '@/object-record/record-field/types/guards/isFieldActor'; import { isFieldRichText } from '@/object-record/record-field/types/guards/isFieldRichText'; + +import { isFieldRichTextV2 } from '@/object-record/record-field/types/guards/isFieldRichTextV2'; import { isDefined } from 'twenty-ui'; import { FieldMetadataType } from '~/generated-metadata/graphql'; @@ -45,7 +47,9 @@ export const isFieldValueReadOnly = ({ if ( isDefined(fieldType) && - (isFieldActor({ type: fieldType }) || isFieldRichText({ type: fieldType })) + (isFieldActor({ type: fieldType }) || + isFieldRichText({ type: fieldType }) || + isFieldRichTextV2({ type: fieldType })) ) { return true; } diff --git a/packages/twenty-front/src/modules/object-record/record-show/components/CardComponents.tsx b/packages/twenty-front/src/modules/object-record/record-show/components/CardComponents.tsx index 2220d2cb8fcf..20a5a02390d3 100644 --- a/packages/twenty-front/src/modules/object-record/record-show/components/CardComponents.tsx +++ b/packages/twenty-front/src/modules/object-record/record-show/components/CardComponents.tsx @@ -53,7 +53,7 @@ export const CardComponents: Record = { ), - [CardType.RichTextCard]: ({ targetableObject }) => ( + [CardType.RichTextV2Card]: ({ targetableObject }) => ( ), diff --git a/packages/twenty-front/src/modules/object-record/record-show/hooks/useRecordShowContainerTabs.ts b/packages/twenty-front/src/modules/object-record/record-show/hooks/useRecordShowContainerTabs.ts index d6a7ce756036..d58d80be8c91 100644 --- a/packages/twenty-front/src/modules/object-record/record-show/hooks/useRecordShowContainerTabs.ts +++ b/packages/twenty-front/src/modules/object-record/record-show/hooks/useRecordShowContainerTabs.ts @@ -36,11 +36,11 @@ export const useRecordShowContainerTabs = ( > = { [CoreObjectNameSingular.Note]: { tabs: { - richText: { + richTextV2: { title: 'Note', position: 0, Icon: IconNotes, - cards: [{ type: CardType.RichTextCard }], + cards: [{ type: CardType.RichTextV2Card }], hide: { ifMobile: false, ifDesktop: false, @@ -56,11 +56,11 @@ export const useRecordShowContainerTabs = ( }, [CoreObjectNameSingular.Task]: { tabs: { - richText: { + richTextV2: { title: 'Note', position: 0, Icon: IconNotes, - cards: [{ type: CardType.RichTextCard }], + cards: [{ type: CardType.RichTextV2Card }], hide: { ifMobile: false, ifDesktop: false, diff --git a/packages/twenty-front/src/modules/object-record/record-show/types/CardType.ts b/packages/twenty-front/src/modules/object-record/record-show/types/CardType.ts index 6a805d0af131..d053d89b9f53 100644 --- a/packages/twenty-front/src/modules/object-record/record-show/types/CardType.ts +++ b/packages/twenty-front/src/modules/object-record/record-show/types/CardType.ts @@ -10,5 +10,5 @@ export enum CardType { WorkflowVersionCard = 'WorkflowVersionCard', WorkflowRunCard = 'WorkflowRunCard', WorkflowRunOutputCard = 'WorkflowRunOutputCard', - RichTextCard = 'RichTextCard', + RichTextV2Card = 'RichTextV2Card', } diff --git a/packages/twenty-front/src/modules/object-record/spreadsheet-import/constants/CompositeFieldImportLabels.ts b/packages/twenty-front/src/modules/object-record/spreadsheet-import/constants/CompositeFieldImportLabels.ts index 030601f241bc..fb04b12fcee3 100644 --- a/packages/twenty-front/src/modules/object-record/spreadsheet-import/constants/CompositeFieldImportLabels.ts +++ b/packages/twenty-front/src/modules/object-record/spreadsheet-import/constants/CompositeFieldImportLabels.ts @@ -5,6 +5,7 @@ import { FieldFullNameValue, FieldLinksValue, FieldPhonesValue, + FieldRichTextV2Value, } from '@/object-record/record-field/types/FieldMetadata'; import { CompositeFieldLabels } from '@/object-record/spreadsheet-import/types/CompositeFieldLabels'; import { FieldMetadataType } from '~/generated-metadata/graphql'; @@ -39,6 +40,10 @@ export const COMPOSITE_FIELD_IMPORT_LABELS = { primaryPhoneCountryCodeLabel: 'Phone country code', primaryPhoneNumberLabel: 'Phone number', } satisfies Partial>, + [FieldMetadataType.RichTextV2]: { + blocknoteLabel: 'BlockNote', + markdownLabel: 'Markdown', + } satisfies Partial>, [FieldMetadataType.Actor]: { sourceLabel: 'Source', }, diff --git a/packages/twenty-front/src/modules/object-record/spreadsheet-import/hooks/useBuildAvailableFieldsForImport.ts b/packages/twenty-front/src/modules/object-record/spreadsheet-import/hooks/useBuildAvailableFieldsForImport.ts index 5dfb80454f67..2bc020e0c78a 100644 --- a/packages/twenty-front/src/modules/object-record/spreadsheet-import/hooks/useBuildAvailableFieldsForImport.ts +++ b/packages/twenty-front/src/modules/object-record/spreadsheet-import/hooks/useBuildAvailableFieldsForImport.ts @@ -211,6 +211,19 @@ export const useBuildAvailableFieldsForImport = () => { ), }); }); + } else if (fieldMetadataItem.type === FieldMetadataType.RichTextV2) { + Object.entries( + COMPOSITE_FIELD_IMPORT_LABELS[FieldMetadataType.RichTextV2], + ).forEach(([_, fieldLabel]) => { + availableFieldsForImport.push({ + icon: getIcon(fieldMetadataItem.icon), + label: `${fieldLabel} (${fieldMetadataItem.label})`, + key: `${fieldLabel} (${fieldMetadataItem.name})`, + fieldType: { + type: 'input', + }, + }); + }); } else { availableFieldsForImport.push({ icon: getIcon(fieldMetadataItem.icon), diff --git a/packages/twenty-front/src/modules/object-record/spreadsheet-import/utils/buildRecordFromImportedStructuredRow.ts b/packages/twenty-front/src/modules/object-record/spreadsheet-import/utils/buildRecordFromImportedStructuredRow.ts index 78bd4310f98b..7ac95c815f09 100644 --- a/packages/twenty-front/src/modules/object-record/spreadsheet-import/utils/buildRecordFromImportedStructuredRow.ts +++ b/packages/twenty-front/src/modules/object-record/spreadsheet-import/utils/buildRecordFromImportedStructuredRow.ts @@ -4,6 +4,7 @@ import { FieldEmailsValue, FieldLinksValue, FieldPhonesValue, + FieldRichTextV2Value, } from '@/object-record/record-field/types/FieldMetadata'; import { COMPOSITE_FIELD_IMPORT_LABELS } from '@/object-record/spreadsheet-import/constants/CompositeFieldImportLabels'; import { ImportedStructuredRow } from '@/spreadsheet-import/types'; @@ -36,6 +37,7 @@ export const buildRecordFromImportedStructuredRow = ( LINKS: { primaryLinkLabelLabel, primaryLinkUrlLabel }, EMAILS: { primaryEmailLabel }, PHONES: { primaryPhoneNumberLabel, primaryPhoneCountryCodeLabel }, + RICH_TEXT_V2: { blocknoteLabel, markdownLabel }, } = COMPOSITE_FIELD_IMPORT_LABELS; for (const field of fields) { @@ -161,6 +163,24 @@ export const buildRecordFromImportedStructuredRow = ( } break; } + case FieldMetadataType.RichTextV2: { + if ( + isDefined( + importedStructuredRow[`${blocknoteLabel} (${field.name})`] || + importedStructuredRow[`${markdownLabel} (${field.name})`], + ) + ) { + recordToBuild[field.name] = { + blocknote: castToString( + importedStructuredRow[`${blocknoteLabel} (${field.name})`], + ), + markdown: castToString( + importedStructuredRow[`${markdownLabel} (${field.name})`], + ), + } satisfies FieldRichTextV2Value; + } + break; + } case FieldMetadataType.Emails: { if ( isDefined( diff --git a/packages/twenty-front/src/modules/object-record/utils/generateEmptyFieldValue.ts b/packages/twenty-front/src/modules/object-record/utils/generateEmptyFieldValue.ts index 7b1059b15eeb..8f5fd425e42b 100644 --- a/packages/twenty-front/src/modules/object-record/utils/generateEmptyFieldValue.ts +++ b/packages/twenty-front/src/modules/object-record/utils/generateEmptyFieldValue.ts @@ -84,6 +84,12 @@ export const generateEmptyFieldValue = ( case FieldMetadataType.RichText: { return null; } + case FieldMetadataType.RichTextV2: { + return { + blocknote: null, + markdown: null, + }; + } case FieldMetadataType.Actor: { return { source: 'MANUAL', diff --git a/packages/twenty-front/src/modules/settings/data-model/constants/SettingsCompositeFieldTypeConfigs.ts b/packages/twenty-front/src/modules/settings/data-model/constants/SettingsCompositeFieldTypeConfigs.ts index c991341b716f..3d048b1b8d8b 100644 --- a/packages/twenty-front/src/modules/settings/data-model/constants/SettingsCompositeFieldTypeConfigs.ts +++ b/packages/twenty-front/src/modules/settings/data-model/constants/SettingsCompositeFieldTypeConfigs.ts @@ -7,6 +7,7 @@ import { FieldFullNameValue, FieldLinksValue, FieldPhonesValue, + FieldRichTextV2Value, } from '@/object-record/record-field/types/FieldMetadata'; import { SettingsFieldTypeConfig } from '@/settings/data-model/constants/SettingsNonCompositeFieldTypeConfigs'; import { CompositeFieldType } from '@/settings/data-model/types/CompositeFieldType'; @@ -17,6 +18,7 @@ import { IllustrationIconMap, IllustrationIconPhone, IllustrationIconSetting, + IllustrationIconText, IllustrationIconUser, } from 'twenty-ui'; import { FieldMetadataType } from '~/generated-metadata/graphql'; @@ -178,4 +180,19 @@ export const SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS = { }, exampleValue: { source: 'source', name: 'name', workspaceMemberId: 'id' }, } as const satisfies SettingsCompositeFieldTypeConfig, + [FieldMetadataType.RichTextV2]: { + label: 'Rich Text', + Icon: IllustrationIconText, + subFields: ['blocknote', 'markdown'], + filterableSubFields: [], + labelBySubField: { + blocknote: 'BlockNote', + markdown: 'Markdown', + }, + exampleValue: { + blocknote: '[{"type":"heading","content":"Hello"}]', + markdown: '# Hello', + }, + category: 'Basic', + } as const satisfies SettingsCompositeFieldTypeConfig, } as const satisfies SettingsCompositeFieldTypeConfigArray; diff --git a/packages/twenty-front/src/modules/settings/data-model/constants/SettingsNonCompositeFieldTypeConfigs.ts b/packages/twenty-front/src/modules/settings/data-model/constants/SettingsNonCompositeFieldTypeConfigs.ts index a762b6f76dcb..a01b5d7f3d3b 100644 --- a/packages/twenty-front/src/modules/settings/data-model/constants/SettingsNonCompositeFieldTypeConfigs.ts +++ b/packages/twenty-front/src/modules/settings/data-model/constants/SettingsNonCompositeFieldTypeConfigs.ts @@ -123,7 +123,7 @@ export const SETTINGS_NON_COMPOSITE_FIELD_TYPE_CONFIGS: SettingsNonCompositeFiel category: 'Advanced', } as const satisfies SettingsFieldTypeConfig, [FieldMetadataType.RichText]: { - label: 'Rich Text', + label: 'Rich Text Deprecated', Icon: IllustrationIconSetting, exampleValue: "{ key: 'value' }", category: 'Basic', diff --git a/packages/twenty-front/src/modules/settings/data-model/types/CompositeFieldType.ts b/packages/twenty-front/src/modules/settings/data-model/types/CompositeFieldType.ts index ddf7b0d579e0..24a134b5a9e8 100644 --- a/packages/twenty-front/src/modules/settings/data-model/types/CompositeFieldType.ts +++ b/packages/twenty-front/src/modules/settings/data-model/types/CompositeFieldType.ts @@ -10,6 +10,7 @@ export const COMPOSITE_FIELD_TYPES = [ 'PHONES', 'FULL_NAME', 'ACTOR', + 'RICH_TEXT_V2', ] as const; type CompositeFieldTypeBaseLiteral = (typeof COMPOSITE_FIELD_TYPES)[number]; diff --git a/packages/twenty-front/src/modules/ui/input/editor/utils/getFirstNonEmptyLineOfRichText.ts b/packages/twenty-front/src/modules/ui/input/editor/utils/getFirstNonEmptyLineOfRichText.ts index 38a1ccceb9e3..4cfafc6cce0a 100644 --- a/packages/twenty-front/src/modules/ui/input/editor/utils/getFirstNonEmptyLineOfRichText.ts +++ b/packages/twenty-front/src/modules/ui/input/editor/utils/getFirstNonEmptyLineOfRichText.ts @@ -2,14 +2,14 @@ import { PartialBlock } from '@blocknote/core'; import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull'; export const getFirstNonEmptyLineOfRichText = ( - fieldValue: PartialBlock[] | null, + blocks: PartialBlock[] | null, ): string => { - if (fieldValue === null) { + if (blocks === null) { return ''; } - for (const node of fieldValue) { - if (!isUndefinedOrNull(node.content)) { - const contentArray = node.content as Array< + for (const block of blocks) { + if (!isUndefinedOrNull(block.content)) { + const contentArray = block.content as Array< { text: string } | { link: string } >; if (contentArray.length > 0) { 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 3b8f482b17fa..63f0e65829a5 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.RichText]: 'IconUsers', + [FieldMetadataType.RichTextV2]: 'IconUsers', [FieldMetadataType.TsVector]: 'IconUsers', }; diff --git a/packages/twenty-front/src/testing/mock-data/notes.ts b/packages/twenty-front/src/testing/mock-data/notes.ts index e1bdcab0b2b6..f9242a7a3769 100644 --- a/packages/twenty-front/src/testing/mock-data/notes.ts +++ b/packages/twenty-front/src/testing/mock-data/notes.ts @@ -9,7 +9,10 @@ export const mockedNotes: Array = [ createdAt: '2023-04-26T10:12:42.33625+00:00', updatedAt: '2023-04-26T10:23:42.33625+00:00', title: 'My very first note', - body: null, + body: { + blocknote: null, + markdown: null, + }, noteTargets: [ { id: '89bb825c-171e-4bcc-9cf7-43448d6fb300', @@ -64,7 +67,10 @@ export const mockedNotes: Array = [ createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), title: 'Another note', - body: null, + body: { + blocknote: null, + markdown: null, + }, noteTargets: [ { id: '89bb825c-171e-4bcc-9cf7-43448d6fb278t', diff --git a/packages/twenty-front/src/testing/mock-data/tasks.ts b/packages/twenty-front/src/testing/mock-data/tasks.ts index aa906a69b254..eb8a193dae78 100644 --- a/packages/twenty-front/src/testing/mock-data/tasks.ts +++ b/packages/twenty-front/src/testing/mock-data/tasks.ts @@ -26,7 +26,10 @@ export const mockedTasks: Array = [ createdAt: '2023-04-26T10:12:42.33625+00:00', updatedAt: '2023-04-26T10:23:42.33625+00:00', title: 'My very first note', - body: null, + body: { + blocknote: null, + markdown: null, + }, dueAt: '2023-04-26T10:12:42.33625+00:00', status: null, assignee: workspaceMember, 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 f8dceb09215e..069b62904062 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 @@ -17,11 +17,11 @@ export class ActivityQueryResultGetterHandler activity: TaskWorkspaceEntity | NoteWorkspaceEntity, workspaceId: string, ): Promise { - if (!activity.id || !activity.body) { + if (!activity.id || !activity.body?.blocknote) { return activity; } - const body: RichTextBody = JSON.parse(activity.body); + const body: RichTextBody = JSON.parse(activity.body.blocknote); const bodyWithSignedPayload = await Promise.all( body.map(async (block: RichTextBlock) => { @@ -51,7 +51,10 @@ export class ActivityQueryResultGetterHandler return { ...activity, - body: JSON.stringify(bodyWithSignedPayload), + body: { + blocknote: JSON.stringify(bodyWithSignedPayload), + markdown: activity.body.markdown, + }, }; } } 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 25cab922556f..6ef3d25c6589 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.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, value]; + } - if (!fieldMetadata) { - return [key, await Promise.resolve(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 000000000000..e99353b16984 --- /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 4970dc3f543b..c53658ffced9 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 96bacd1629a6..885d215c105e 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 d583693c67e7..1507e8a0921d 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 @@ -14,6 +14,7 @@ export enum FeatureFlagKey { IsAdvancedFiltersEnabled = 'IS_ADVANCED_FILTERS_ENABLED', IsAggregateQueryEnabled = 'IS_AGGREGATE_QUERY_ENABLED', IsViewGroupsEnabled = 'IS_VIEW_GROUPS_ENABLED', + IsRichTextV2Enabled = 'IS_RICH_TEXT_V2_ENABLED', IsCommandMenuV2Enabled = 'IS_COMMAND_MENU_V2_ENABLED', IsCrmMigrationEnabled = 'IS_CRM_MIGRATION_ENABLED', IsJsonFilterEnabled = 'IS_JSON_FILTER_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 e01c55257ad2..f32903d72610 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 f7f943b825ca..c9ddcdd53a53 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.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.composite-type.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/composite-types/rich-text.composite-type.ts new file mode 100644 index 000000000000..0d7a98cedaab --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/composite-types/rich-text.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 68b88bc28fd0..4479d0260e01 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/generate-default-value.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/generate-default-value.ts index bbb307ebf2cb..d2e3d2bd77a3 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 0fb1c6968ebf..172751bb0fd4 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 efa979312b3a..c495836e8602 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/constants/fieldMetadataTypesToTextColumnType.ts b/packages/twenty-server/src/engine/metadata-modules/workspace-migration/constants/fieldMetadataTypesToTextColumnType.ts index 7a447a9b8b7d..e8a984e69ce1 100644 --- a/packages/twenty-server/src/engine/metadata-modules/workspace-migration/constants/fieldMetadataTypesToTextColumnType.ts +++ b/packages/twenty-server/src/engine/metadata-modules/workspace-migration/constants/fieldMetadataTypesToTextColumnType.ts @@ -2,6 +2,6 @@ import { FieldMetadataType } from 'twenty-shared'; export const FIELD_METADATA_TYPES_TO_TEXT_COLUMN_TYPE = [ FieldMetadataType.TEXT, - FieldMetadataType.RICH_TEXT, + FieldMetadataType.RICH_TEXT_V2, FieldMetadataType.ARRAY, ]; 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 941403074b21..830e62281179 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/utils/field-metadata-type-to-column-type.util.ts b/packages/twenty-server/src/engine/metadata-modules/workspace-migration/utils/field-metadata-type-to-column-type.util.ts index c9777c2f5c7b..eb5b4ec87ae2 100644 --- a/packages/twenty-server/src/engine/metadata-modules/workspace-migration/utils/field-metadata-type-to-column-type.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/workspace-migration/utils/field-metadata-type-to-column-type.util.ts @@ -38,6 +38,8 @@ export const fieldMetadataTypeToColumnType = ( return 'jsonb'; case FieldMetadataType.TS_VECTOR: return 'tsvector'; + case FieldMetadataType.RICH_TEXT: + return 'text'; default: throw new WorkspaceMigrationException( `Cannot convert ${fieldMetadataType} to column type.`, diff --git a/packages/twenty-server/src/engine/metadata-modules/workspace-migration/utils/is-text-column-type.util.ts b/packages/twenty-server/src/engine/metadata-modules/workspace-migration/utils/is-text-column-type.util.ts index 6dc0b0ef88a6..62a39aaea18c 100644 --- a/packages/twenty-server/src/engine/metadata-modules/workspace-migration/utils/is-text-column-type.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/workspace-migration/utils/is-text-column-type.util.ts @@ -3,7 +3,7 @@ import { FieldMetadataType } from 'twenty-shared'; export const isTextColumnType = (type: FieldMetadataType) => { return ( type === FieldMetadataType.TEXT || - type === FieldMetadataType.RICH_TEXT || + type === FieldMetadataType.RICH_TEXT_V2 || type === FieldMetadataType.ARRAY ); }; 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 77d7a8351ae8..a231a029a53c 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 a33a96afdd05..4000ea2ad02c 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/utils/is-searchable-field.util.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/utils/is-searchable-field.util.ts index c1dd3a3cd567..a7d775190b7f 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/modules/note/standard-objects/note.workspace-entity.ts b/packages/twenty-server/src/modules/note/standard-objects/note.workspace-entity.ts index bff384c2d757..a100a9a7f86b 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 @@ -7,6 +7,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.composite-type'; import { IndexType } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity'; import { RelationMetadataType, @@ -36,7 +37,7 @@ 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 }, + // { name: BODY_FIELD_NAME, type: FieldMetadataType.RICH_TEXT_V2 }, // TODO: Check later if and how this works ]; @WorkspaceEntity({ @@ -72,13 +73,13 @@ export class NoteWorkspaceEntity extends BaseWorkspaceEntity { @WorkspaceField({ standardId: NOTE_STANDARD_FIELD_IDS.body, - type: FieldMetadataType.RICH_TEXT, + type: FieldMetadataType.RICH_TEXT_V2, label: 'Body', description: 'Note body', icon: 'IconFilePencil', }) @WorkspaceIsNullable() - [BODY_FIELD_NAME]: string | null; + [BODY_FIELD_NAME]: RichTextV2Metadata | null; @WorkspaceField({ standardId: NOTE_STANDARD_FIELD_IDS.createdBy, 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 73557f8067fb..ee0e60e5c179 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 @@ -7,6 +7,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.composite-type'; import { IndexType } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity'; import { RelationMetadataType, @@ -38,7 +39,7 @@ 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 }, + // { name: BODY_FIELD_NAME, type: FieldMetadataType.RICH_TEXT_V2 }, // TODO: Check later if and how this works ]; @WorkspaceEntity({ @@ -74,13 +75,13 @@ export class TaskWorkspaceEntity extends BaseWorkspaceEntity { @WorkspaceField({ standardId: TASK_STANDARD_FIELD_IDS.body, - type: FieldMetadataType.RICH_TEXT, + type: FieldMetadataType.RICH_TEXT_V2, label: 'Body', description: 'Task body', icon: 'IconFilePencil', }) @WorkspaceIsNullable() - [BODY_FIELD_NAME]: string | null; + [BODY_FIELD_NAME]: RichTextV2Metadata | null; @WorkspaceField({ standardId: TASK_STANDARD_FIELD_IDS.dueAt, diff --git a/packages/twenty-shared/src/types/FieldMetadataType.ts b/packages/twenty-shared/src/types/FieldMetadataType.ts index 83589ce928bb..686da7e67fea 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/packages/twenty-zapier/src/utils/computeInputFields.ts b/packages/twenty-zapier/src/utils/computeInputFields.ts index 570c0b9323c0..f06d437d32c9 100644 --- a/packages/twenty-zapier/src/utils/computeInputFields.ts +++ b/packages/twenty-zapier/src/utils/computeInputFields.ts @@ -200,6 +200,25 @@ const get_subfieldsFromField = (nodeField: NodeField): NodeField[] => { }; return [primaryLinkLabel, primaryLinkUrl, secondaryLinks]; } + case FieldMetadataType.RICH_TEXT_V2: { + const blocknote: NodeField = { + type: FieldMetadataType.TEXT, + name: 'blocknote', + label: 'Blocknote', + description: 'Blocknote', + isNullable: true, + defaultValue: null, + }; + const markdown: NodeField = { + type: FieldMetadataType.TEXT, + name: 'markdown', + label: 'Markdown', + description: 'Markdown', + isNullable: true, + defaultValue: null, + }; + return [blocknote, markdown]; + } default: throw new Error(`Unknown nodeField type: ${nodeField.type}`); } @@ -223,6 +242,7 @@ export const computeInputFields = ( case FieldMetadataType.EMAILS: case FieldMetadataType.LINKS: case FieldMetadataType.ADDRESS: + case FieldMetadataType.RICH_TEXT_V2: for (const subNodeField of get_subfieldsFromField(nodeField)) { const field = { key: `${nodeField.name}__${subNodeField.name}`, diff --git a/yarn.lock b/yarn.lock index 352de163adde..169ba733cb1b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3247,6 +3247,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" @@ -3315,6 +3364,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" @@ -3332,6 +3400,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" @@ -15288,6 +15377,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" @@ -32347,6 +32446,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" @@ -39451,7 +39589,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: @@ -39487,7 +39625,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: @@ -39954,7 +40092,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: @@ -45083,6 +45221,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" @@ -48037,6 +48176,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"