(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"